use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use tldr_core::analysis::references::{
find_references, ReferenceKind, ReferencesOptions, ReferencesReport, SearchScope,
};
use crate::output::{common_path_prefix, strip_prefix_display, OutputFormat, OutputWriter};
#[derive(Debug, Args)]
pub struct ReferencesArgs {
pub symbol: String,
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long = "output", short = 'o', hide = true)]
pub output: Option<String>,
#[arg(long, short = 'l')]
pub lang: Option<String>,
#[arg(long)]
pub include_definition: bool,
#[arg(long, short = 't')]
pub kinds: Option<String>,
#[arg(long, short = 's', default_value = "workspace")]
pub scope: String,
#[arg(long, short = 'n', default_value = "20")]
pub limit: usize,
#[arg(long, short = 'C', default_value = "0")]
pub context_lines: usize,
#[arg(long, default_value = "0.0")]
pub min_confidence: f64,
}
impl ReferencesArgs {
pub fn run(&self, cli_format: OutputFormat, quiet: bool) -> Result<()> {
if !self.path.exists() {
anyhow::bail!(
"Path not found: '{}'. Please provide a valid file or directory.",
self.path.display()
);
}
let output_format = match self.output.as_deref() {
Some("text") => OutputFormat::Text,
Some("compact") => OutputFormat::Compact,
Some("json") => OutputFormat::Json,
_ => cli_format,
};
let writer = OutputWriter::new(output_format, quiet);
let kinds = self.kinds.as_ref().map(|k| parse_kinds(k));
let scope = parse_scope(&self.scope);
let options = ReferencesOptions {
include_definition: self.include_definition,
kinds,
scope,
language: self.lang.clone(),
limit: Some(self.limit),
definition_file: None,
context_lines: self.context_lines,
};
writer.progress(&format!(
"Finding references to '{}' in {}...",
self.symbol,
self.path.display()
));
let report = find_references(&self.symbol, &self.path, &options)?;
let report = filter_by_min_confidence(report, self.min_confidence);
match output_format {
OutputFormat::Text => {
let text = format_references_text(&report);
writer.write_text(&text)?;
}
_ => {
writer.write(&report)?;
}
}
if report.total_references == 0 && !quiet {
eprintln!();
eprintln!(
"No references found for '{}'. Searched {} files.",
self.symbol, report.stats.files_searched
);
eprintln!("Suggestions:");
eprintln!(" - Check the symbol spelling");
eprintln!(" - Try a different search scope with --scope workspace");
eprintln!(" - Verify the path contains relevant source files");
}
Ok(())
}
}
fn parse_kinds(s: &str) -> Vec<ReferenceKind> {
s.split(',')
.filter_map(|k| match k.trim().to_lowercase().as_str() {
"call" => Some(ReferenceKind::Call),
"read" => Some(ReferenceKind::Read),
"write" => Some(ReferenceKind::Write),
"import" => Some(ReferenceKind::Import),
"type" => Some(ReferenceKind::Type),
"definition" => Some(ReferenceKind::Definition),
"other" => Some(ReferenceKind::Other),
_ => None,
})
.collect()
}
fn filter_by_min_confidence(mut report: ReferencesReport, min_confidence: f64) -> ReferencesReport {
if min_confidence > 0.0 {
report
.references
.retain(|r| r.confidence.unwrap_or(0.0) >= min_confidence);
report.total_references = report.references.len();
}
report
}
fn parse_scope(s: &str) -> SearchScope {
match s.to_lowercase().as_str() {
"local" => SearchScope::Local,
"file" => SearchScope::File,
_ => SearchScope::Workspace,
}
}
fn format_references_text(report: &ReferencesReport) -> String {
use std::path::Path;
let mut output = String::new();
let mut all_paths: Vec<&Path> = report.references.iter().map(|r| r.file.as_path()).collect();
if let Some(def) = &report.definition {
all_paths.push(def.file.as_path());
}
let prefix = if all_paths.is_empty() {
PathBuf::new()
} else {
common_path_prefix(&all_paths)
};
output.push_str(&format!(
"References to: {} ({})\n",
report.symbol,
report
.definition
.as_ref()
.map(|d| d.kind.as_str())
.unwrap_or("unknown")
));
output.push('\n');
if let Some(def) = &report.definition {
output.push_str("Definition:\n");
let def_display = strip_prefix_display(&def.file, &prefix);
output.push_str(&format!(
" {}:{}:{} [{}]\n",
def_display,
def.line,
def.column,
def.kind.as_str()
));
if let Some(sig) = &def.signature {
let sig_clean = sig.replace('\t', " ");
output.push_str(&format!(" {}\n", sig_clean.trim()));
}
output.push('\n');
}
output.push_str(&format!(
"References ({} found in {}ms):\n",
report.total_references, report.stats.search_time_ms
));
for r in &report.references {
let ref_display = strip_prefix_display(&r.file, &prefix);
output.push_str(&format!(
" {}:{}:{} [{}]\n",
ref_display,
r.line,
r.column,
r.kind.as_str()
));
let context_clean = r.context.replace('\t', " ");
output.push_str(&format!(" {}\n", context_clean.trim()));
output.push('\n');
}
output.push_str(&format!(
"Search: {} files, {} candidates -> {} verified\n",
report.stats.files_searched,
report.stats.candidates_found,
report.stats.verified_references
));
output.push_str(&format!("Scope: {}\n", report.search_scope.as_str()));
output
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tldr_core::analysis::references::{Definition, DefinitionKind, Reference, ReferenceStats};
fn make_test_report() -> ReferencesReport {
ReferencesReport {
symbol: "test_func".to_string(),
definition: Some(Definition {
file: PathBuf::from("src/lib.py"),
line: 42,
column: 5,
kind: DefinitionKind::Function,
signature: Some("def test_func(x: int) -> str:".to_string()),
}),
references: vec![
Reference::new(
PathBuf::from("src/main.py"),
10,
8,
ReferenceKind::Call,
"result = test_func(42)".to_string(),
),
Reference::new(
PathBuf::from("tests/test_lib.py"),
25,
12,
ReferenceKind::Import,
"from src.lib import test_func".to_string(),
),
],
total_references: 2,
search_scope: SearchScope::Workspace,
stats: ReferenceStats {
files_searched: 10,
candidates_found: 5,
verified_references: 2,
search_time_ms: 127,
},
}
}
#[test]
fn test_format_references_text() {
let report = make_test_report();
let text = format_references_text(&report);
assert!(text.contains("References to: test_func (function)"));
assert!(text.contains("Definition:"));
assert!(text.contains("src/lib.py:42:5 [function]"));
assert!(text.contains("def test_func(x: int) -> str:"));
assert!(text.contains("References (2 found in 127ms)"));
assert!(text.contains("src/main.py:10:8 [call]"));
assert!(text.contains("tests/test_lib.py:25:12 [import]"));
assert!(text.contains("Search: 10 files, 5 candidates -> 2 verified"));
assert!(text.contains("Scope: workspace"));
}
#[test]
fn test_parse_kinds() {
let kinds = parse_kinds("call,import,type");
assert_eq!(kinds.len(), 3);
assert!(kinds.contains(&ReferenceKind::Call));
assert!(kinds.contains(&ReferenceKind::Import));
assert!(kinds.contains(&ReferenceKind::Type));
}
#[test]
fn test_parse_kinds_case_insensitive() {
let kinds = parse_kinds("CALL,Read,WRITE");
assert_eq!(kinds.len(), 3);
assert!(kinds.contains(&ReferenceKind::Call));
assert!(kinds.contains(&ReferenceKind::Read));
assert!(kinds.contains(&ReferenceKind::Write));
}
#[test]
fn test_parse_scope() {
assert_eq!(parse_scope("local"), SearchScope::Local);
assert_eq!(parse_scope("file"), SearchScope::File);
assert_eq!(parse_scope("workspace"), SearchScope::Workspace);
assert_eq!(parse_scope("WORKSPACE"), SearchScope::Workspace);
assert_eq!(parse_scope("unknown"), SearchScope::Workspace); }
#[test]
fn test_tab_expansion_in_context() {
let mut report = make_test_report();
report.references[0] = Reference::new(
PathBuf::from("src/main.py"),
10,
8,
ReferenceKind::Call,
"\tresult = test_func(42)".to_string(), );
let text = format_references_text(&report);
assert!(text.contains(" result = test_func(42)"));
assert!(!text.contains('\t'));
}
#[test]
fn test_text_formatter_strips_common_path_prefix() {
let mut report = make_test_report();
report.definition = Some(Definition {
file: PathBuf::from("/home/user/project/src/lib.py"),
line: 42,
column: 5,
kind: DefinitionKind::Function,
signature: Some("def test_func(x: int) -> str:".to_string()),
});
report.references = vec![
Reference::new(
PathBuf::from("/home/user/project/src/main.py"),
10,
8,
ReferenceKind::Call,
"result = test_func(42)".to_string(),
),
Reference::new(
PathBuf::from("/home/user/project/tests/test_lib.py"),
25,
12,
ReferenceKind::Import,
"from src.lib import test_func".to_string(),
),
];
let text = format_references_text(&report);
assert!(
!text.contains("/home/user/project/"),
"Text should not contain the absolute common prefix. Got:\n{}",
text
);
assert!(text.contains("src/lib.py:42:5"));
assert!(text.contains("src/main.py:10:8"));
assert!(text.contains("tests/test_lib.py:25:12"));
}
#[test]
fn test_default_limit_is_20() {
use clap::Parser;
#[derive(Parser)]
struct Wrapper {
#[command(flatten)]
refs: ReferencesArgs,
}
let wrapper = Wrapper::parse_from(["test", "my_symbol"]);
assert_eq!(
wrapper.refs.limit, 20,
"Default limit should be 20, got {}",
wrapper.refs.limit
);
}
#[test]
fn test_min_confidence_filtering() {
let report = ReferencesReport {
symbol: "test_func".to_string(),
definition: None,
references: vec![
Reference::with_details(
PathBuf::from("src/a.py"),
10,
1,
10,
ReferenceKind::Call,
"test_func()".to_string(),
1.0, ),
Reference::with_details(
PathBuf::from("src/b.py"),
20,
1,
10,
ReferenceKind::Call,
"test_func()".to_string(),
0.5, ),
Reference::with_details(
PathBuf::from("src/c.py"),
30,
1,
10,
ReferenceKind::Call,
"test_func()".to_string(),
0.3, ),
],
total_references: 3,
search_scope: SearchScope::Workspace,
stats: ReferenceStats {
files_searched: 5,
candidates_found: 3,
verified_references: 3,
search_time_ms: 50,
},
};
let filtered = filter_by_min_confidence(report.clone(), 0.5);
assert_eq!(
filtered.references.len(),
2,
"Should have 2 refs with confidence >= 0.5, got {}",
filtered.references.len()
);
assert_eq!(
filtered.total_references, 2,
"total_references should be updated after filtering"
);
let filtered_high = filter_by_min_confidence(report.clone(), 1.0);
assert_eq!(filtered_high.references.len(), 1);
assert_eq!(filtered_high.total_references, 1);
let filtered_none = filter_by_min_confidence(report, 0.0);
assert_eq!(filtered_none.references.len(), 3);
assert_eq!(filtered_none.total_references, 3);
}
#[test]
fn test_kinds_short_flag_t() {
use clap::Parser;
#[derive(Parser)]
struct Wrapper {
#[command(flatten)]
refs: ReferencesArgs,
}
let wrapper = Wrapper::parse_from(["test", "my_symbol", ".", "-t", "call,import"]);
assert_eq!(
wrapper.refs.kinds.as_deref(),
Some("call,import"),
"--kinds should be settable via -t short flag"
);
}
}