use anyhow::{bail, Result};
use clap::Args;
use std::collections::HashMap;
use std::path::Path;
use crate::config::project::is_vendor_path;
use crate::config::workspace::WorkspaceConfig;
use crate::config::ProjectConfig;
use crate::core::graph::Graph;
use crate::core::graph::Reference;
use crate::core::workspace_graph::WorkspaceGraph;
use crate::output::formatter;
use crate::output::json::JsonOutput;
use crate::Context;
#[derive(Args, Debug)]
pub struct RefsArgs {
pub symbol: String,
#[arg(long)]
pub kind: Option<String>,
#[arg(long, default_value = "20")]
pub limit: usize,
#[arg(long, short = 'c', default_value = "0")]
pub context: usize,
#[arg(long, short = 'j')]
pub json: bool,
}
#[derive(Args, Debug)]
pub struct CallersArgs {
pub symbol: String,
#[arg(long, default_value = "1")]
pub depth: usize,
#[arg(long, default_value = "20")]
pub limit: usize,
#[arg(long, short = 'c', default_value = "0")]
pub context: usize,
#[arg(long, short = 'j')]
pub json: bool,
}
pub fn run_callers(args: &CallersArgs, project_root: &Path) -> Result<()> {
if args.depth > 1 {
return run_callers_transitive(args, project_root, "callers");
}
let refs_args = RefsArgs {
symbol: args.symbol.clone(),
kind: Some("calls".to_string()),
limit: args.limit,
context: args.context,
json: args.json,
};
let ctx = Context::SingleProject {
root: project_root.to_path_buf(),
};
run(&refs_args, &ctx)
}
pub(super) fn run_callers_transitive(
args: &CallersArgs,
project_root: &Path,
command_label: &'static str,
) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
bail!("No .scope/ directory found. Run 'scope init' first.");
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
bail!("No index found. Run 'scope index' to build one first.");
}
let graph = Graph::open(&db_path)?;
crate::commands::warn_if_stale(&graph, project_root);
let result = if looks_like_file_path(&args.symbol) {
let file_path = formatter::normalize_path(&args.symbol);
graph.find_file_impact(&file_path, args.depth)?
} else {
graph.find_impact(&args.symbol, args.depth)?
};
if args.json {
let output = JsonOutput {
command: command_label,
symbol: Some(args.symbol.clone()),
data: &result,
truncated: false,
total: result.total_affected + result.test_files.len(),
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_impact(&args.symbol, &result);
}
Ok(())
}
fn enrich_refs_with_snippets(refs: &mut [Reference], project_root: &Path, context_lines: usize) {
let mut by_file: HashMap<String, Vec<usize>> = HashMap::new();
for (i, r) in refs.iter().enumerate() {
by_file.entry(r.file_path.clone()).or_default().push(i);
}
for (file_path, indices) in &by_file {
let full_path = project_root.join(file_path);
let lines = match std::fs::read_to_string(&full_path) {
Ok(content) => content.lines().map(String::from).collect::<Vec<_>>(),
Err(_) => continue, };
for &idx in indices {
let r = &mut refs[idx];
let Some(line_num) = r.line else { continue };
let line_idx = (line_num as usize).saturating_sub(1);
if line_idx >= lines.len() {
continue;
}
r.snippet_line = Some(lines[line_idx].trim_end().to_string());
if context_lines > 0 {
let start = line_idx.saturating_sub(context_lines);
let end = (line_idx + context_lines + 1).min(lines.len());
let ctx: Vec<String> = lines[start..end]
.iter()
.map(|l| l.trim_end().to_string())
.collect();
r.snippet = Some(ctx);
}
}
}
}
use super::looks_like_file_path;
fn derank_vendor_refs(refs: Vec<Reference>, vendor_patterns: &[String]) -> Vec<Reference> {
if vendor_patterns.is_empty() {
return refs;
}
let (first_party, vendor): (Vec<_>, Vec<_>) = refs
.into_iter()
.partition(|r| !is_vendor_path(&r.file_path, vendor_patterns));
let mut combined = first_party;
combined.extend(vendor);
combined
}
pub fn run(args: &RefsArgs, ctx: &Context) -> Result<()> {
match ctx {
Context::SingleProject { root } => run_single(args, root),
Context::Workspace {
workspace_root,
config,
..
} => run_workspace(args, workspace_root, config),
}
}
fn run_single(args: &RefsArgs, project_root: &Path) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
bail!("No .scope/ directory found. Run 'scope init' first.");
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
bail!("No index found. Run 'scope index' to build one first.");
}
let graph = Graph::open(&db_path)?;
crate::commands::warn_if_stale(&graph, project_root);
if looks_like_file_path(&args.symbol) {
return run_file_refs(args, &graph, project_root);
}
run_symbol_refs(args, &graph, project_root)
}
fn run_symbol_refs(args: &RefsArgs, graph: &Graph, project_root: &Path) -> Result<()> {
let kinds: Option<Vec<&str>> = args.kind.as_deref().map(|k| vec![k]);
let kinds_slice = kinds.as_deref();
let vendor_patterns = ProjectConfig::load(&project_root.join(".scope"))
.map(|c| c.index.vendor_patterns)
.unwrap_or_default();
let is_class = graph.is_class_like(&args.symbol)?;
if is_class && kinds_slice.is_none() {
let (mut groups, total) = graph.find_refs_grouped(&args.symbol, args.limit)?;
for (_kind, refs) in &mut groups {
let taken = std::mem::take(refs);
*refs = derank_vendor_refs(taken, &vendor_patterns);
}
for (_kind, refs) in &mut groups {
enrich_refs_with_snippets(refs, project_root, args.context);
}
if args.json {
let data = serde_json::json!({
"groups": groups.iter().map(|(kind, refs)| {
serde_json::json!({
"kind": kind,
"refs": refs,
})
}).collect::<Vec<_>>(),
});
let output = JsonOutput {
command: "refs",
symbol: Some(args.symbol.clone()),
data,
truncated: total > args.limit,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_refs_grouped(&args.symbol, &groups, total);
}
} else {
let (refs, total) = graph.find_refs(&args.symbol, kinds_slice, args.limit)?;
let mut refs = derank_vendor_refs(refs, &vendor_patterns);
enrich_refs_with_snippets(&mut refs, project_root, args.context);
if args.json {
let output = JsonOutput {
command: "refs",
symbol: Some(args.symbol.clone()),
data: &refs,
truncated: total > args.limit,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_refs(&args.symbol, &refs, total);
}
}
Ok(())
}
fn run_file_refs(args: &RefsArgs, graph: &Graph, project_root: &Path) -> Result<()> {
let file_path = formatter::normalize_path(&args.symbol);
let kinds: Option<Vec<&str>> = args.kind.as_deref().map(|k| vec![k]);
let kinds_slice = kinds.as_deref();
let (mut refs, total) = graph.find_file_refs(&file_path, kinds_slice, args.limit)?;
enrich_refs_with_snippets(&mut refs, project_root, args.context);
if args.json {
let output = JsonOutput {
command: "refs",
symbol: Some(file_path.clone()),
data: &refs,
truncated: total > args.limit,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_file_refs(&file_path, &refs, total);
}
Ok(())
}
fn run_workspace(args: &RefsArgs, workspace_root: &Path, config: &WorkspaceConfig) -> Result<()> {
let members: Vec<(String, std::path::PathBuf)> = config
.workspace
.members
.iter()
.map(|entry| {
let name = WorkspaceConfig::resolve_member_name(entry);
let path = workspace_root.join(&entry.path);
(name, path)
})
.collect();
let wg = WorkspaceGraph::open(members)?;
let kinds: Option<Vec<&str>> = args.kind.as_deref().map(|k| vec![k]);
let kinds_slice = kinds.as_deref();
let ws_refs = wg.find_refs(&args.symbol, kinds_slice, args.limit);
if ws_refs.is_empty() {
if args.json {
let output = JsonOutput {
command: "refs",
symbol: Some(args.symbol.clone()),
data: &Vec::<serde_json::Value>::new(),
truncated: false,
total: 0,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("No references to '{}' found across workspace.", args.symbol);
}
return Ok(());
}
let total = ws_refs.len();
if args.json {
let output = JsonOutput {
command: "refs",
symbol: Some(args.symbol.clone()),
data: &ws_refs,
truncated: false,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_workspace_refs(&args.symbol, &ws_refs, total);
}
Ok(())
}