|
|
|
@@ -0,0 +1,672 @@
|
|
|
|
|
//! The `defsnip` command extracts full AST-backed definitions by exact symbol name.
|
|
|
|
|
|
|
|
|
|
use codeindex::{
|
|
|
|
|
CodeIndexer, CodeLanguage, IndexedSymbol, SUPPORTED_LANGUAGE_LIST, SymbolKind, detect_language,
|
|
|
|
|
parse_language_label,
|
|
|
|
|
};
|
|
|
|
|
use common::{
|
|
|
|
|
CliError, CommonArgs, ExitCode, InputFormat, RenderMode, collect_matching_files,
|
|
|
|
|
parse_color_choice, parse_format_choice, parse_input_format, print_json,
|
|
|
|
|
print_quick_help_error, print_structured, should_read_stdin,
|
|
|
|
|
};
|
|
|
|
|
use lexopt::prelude::{Long, Short, Value as ArgValue};
|
|
|
|
|
use std::collections::BTreeSet;
|
|
|
|
|
use std::ffi::OsString;
|
|
|
|
|
use std::fmt::Write as _;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::io::{self, Read};
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
const HELP: &str = "\
|
|
|
|
|
Extract full code definitions by exact symbol name via the shared codeindex engine.
|
|
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
defsnip [OPTIONS] <SYMBOL> [PATH...]
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
--format <FORMAT> Structured output format: text, json, toon
|
|
|
|
|
--json Shortcut for --format json
|
|
|
|
|
--toon Shortcut for --format toon
|
|
|
|
|
--input-format <FORMAT> Override stdin parsing mode: auto, lines, jsonl
|
|
|
|
|
--color <WHEN> Control ANSI color output: auto, never
|
|
|
|
|
--quiet Suppress non-essential status output
|
|
|
|
|
--lang <LIST> Restrict languages: rust,csharp,powershell,python,go,java,javascript,typescript
|
|
|
|
|
--kind <LIST> Restrict kinds: module,namespace,class,struct,enum,interface,record,trait,impl,type_alias,function,method,constructor,const,static
|
|
|
|
|
--limit <COUNT> Maximum number of matching definitions to emit
|
|
|
|
|
--allow-empty Exit 0 when no matching definitions are found
|
|
|
|
|
--parents Include parent-chain metadata in text output
|
|
|
|
|
-h, --help Show this help text
|
|
|
|
|
-V, --version Show the command version
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
defsnip helper .\\fixtures\\polyglot\\repo
|
|
|
|
|
defsnip build_report . --json | ConvertFrom-Json
|
|
|
|
|
defsnip --kind method --parents Build .\\fixtures\\polyglot\\repo
|
|
|
|
|
'.\\fixtures\\polyglot\\repo\\web\\app.ts' | defsnip helper --json | ConvertFrom-Json
|
|
|
|
|
|
|
|
|
|
Notes:
|
|
|
|
|
exact-name lookups can return multiple definitions; narrow with PATH, --kind, or --lang
|
|
|
|
|
|
|
|
|
|
JSON fields:
|
|
|
|
|
engine, path, language, kind, name, qualified_name, signature, start_line, end_line, text
|
|
|
|
|
";
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct Cli {
|
|
|
|
|
common: CommonArgs,
|
|
|
|
|
symbol: String,
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
languages: Option<BTreeSet<CodeLanguage>>,
|
|
|
|
|
kinds: Option<BTreeSet<SymbolKind>>,
|
|
|
|
|
limit: usize,
|
|
|
|
|
allow_empty: bool,
|
|
|
|
|
parents: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct CliDraft {
|
|
|
|
|
common: CommonArgs,
|
|
|
|
|
paths: Vec<PathBuf>,
|
|
|
|
|
languages: Option<BTreeSet<CodeLanguage>>,
|
|
|
|
|
kinds: Option<BTreeSet<SymbolKind>>,
|
|
|
|
|
limit: usize,
|
|
|
|
|
allow_empty: bool,
|
|
|
|
|
parents: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
|
|
|
enum ParseOutcome {
|
|
|
|
|
Help,
|
|
|
|
|
Version,
|
|
|
|
|
Run,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Parses CLI arguments and returns a process exit code.
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn main_entry() -> i32 {
|
|
|
|
|
match parse_cli_from(std::env::args_os()) {
|
|
|
|
|
Ok((ParseOutcome::Help, _)) => {
|
|
|
|
|
print!("{HELP}");
|
|
|
|
|
ExitCode::Success.as_i32()
|
|
|
|
|
}
|
|
|
|
|
Ok((ParseOutcome::Version, _)) => {
|
|
|
|
|
println!("defsnip {}", env!("CARGO_PKG_VERSION"));
|
|
|
|
|
ExitCode::Success.as_i32()
|
|
|
|
|
}
|
|
|
|
|
Ok((ParseOutcome::Run, cli)) => match run(&cli) {
|
|
|
|
|
Ok(code) => code.as_i32(),
|
|
|
|
|
Err(error) => {
|
|
|
|
|
print_quick_help_error(&error, HELP);
|
|
|
|
|
error.exit_code().as_i32()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(error) => {
|
|
|
|
|
print_quick_help_error(&error, HELP);
|
|
|
|
|
error.exit_code().as_i32()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_cli_from<I, T>(args: I) -> Result<(ParseOutcome, Cli), CliError>
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = T>,
|
|
|
|
|
T: Into<OsString>,
|
|
|
|
|
{
|
|
|
|
|
let mut parser = lexopt::Parser::from_iter(args);
|
|
|
|
|
let mut draft = CliDraft {
|
|
|
|
|
common: CommonArgs::default(),
|
|
|
|
|
paths: Vec::new(),
|
|
|
|
|
languages: None,
|
|
|
|
|
kinds: None,
|
|
|
|
|
limit: 20,
|
|
|
|
|
allow_empty: false,
|
|
|
|
|
parents: false,
|
|
|
|
|
};
|
|
|
|
|
let mut symbol = None::<String>;
|
|
|
|
|
|
|
|
|
|
while let Some(argument) = parser
|
|
|
|
|
.next()
|
|
|
|
|
.map_err(|error| CliError::usage(error.to_string()))?
|
|
|
|
|
{
|
|
|
|
|
match argument {
|
|
|
|
|
Long("help") | Short('h') => {
|
|
|
|
|
return Ok((ParseOutcome::Help, draft.clone().into_cli(String::new())));
|
|
|
|
|
}
|
|
|
|
|
Long("version") | Short('V') => {
|
|
|
|
|
return Ok((ParseOutcome::Version, draft.clone().into_cli(String::new())));
|
|
|
|
|
}
|
|
|
|
|
Long("json") => draft.common.set_render_mode(RenderMode::Json),
|
|
|
|
|
Long("toon") => draft.common.set_render_mode(RenderMode::Toon),
|
|
|
|
|
Long("format") => {
|
|
|
|
|
let value = parser_value_string(&mut parser, "--format")?;
|
|
|
|
|
draft.common.set_render_mode(parse_format_choice(&value)?);
|
|
|
|
|
}
|
|
|
|
|
Long("input-format") => {
|
|
|
|
|
draft.common.input_format =
|
|
|
|
|
parse_input_format(&parser_value_string(&mut parser, "--input-format")?)?;
|
|
|
|
|
}
|
|
|
|
|
Long("color") => {
|
|
|
|
|
draft.common.color =
|
|
|
|
|
parse_color_choice(&parser_value_string(&mut parser, "--color")?)?;
|
|
|
|
|
}
|
|
|
|
|
Long("quiet") => draft.common.quiet = true,
|
|
|
|
|
Long("lang") => {
|
|
|
|
|
draft.languages = Some(parse_language_list(&parser_value_string(
|
|
|
|
|
&mut parser,
|
|
|
|
|
"--lang",
|
|
|
|
|
)?)?);
|
|
|
|
|
}
|
|
|
|
|
Long("kind") => {
|
|
|
|
|
draft.kinds = Some(parse_kind_list(&parser_value_string(
|
|
|
|
|
&mut parser,
|
|
|
|
|
"--kind",
|
|
|
|
|
)?)?);
|
|
|
|
|
}
|
|
|
|
|
Long("limit") => {
|
|
|
|
|
draft.limit = parse_positive_usize_flag(
|
|
|
|
|
"--limit",
|
|
|
|
|
&parser_value_string(&mut parser, "--limit")?,
|
|
|
|
|
)?;
|
|
|
|
|
}
|
|
|
|
|
Long("allow-empty") => draft.allow_empty = true,
|
|
|
|
|
Long("parents") => draft.parents = true,
|
|
|
|
|
ArgValue(value) => {
|
|
|
|
|
if symbol.is_none() {
|
|
|
|
|
symbol = Some(os_value_string(value, "symbol")?);
|
|
|
|
|
} else {
|
|
|
|
|
draft.paths.push(PathBuf::from(value));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
return Err(CliError::usage(
|
|
|
|
|
"unsupported argument; use --help to see available options",
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let symbol =
|
|
|
|
|
symbol.ok_or_else(|| CliError::usage("provide an exact symbol name to extract"))?;
|
|
|
|
|
Ok((ParseOutcome::Run, draft.into_cli(symbol)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl CliDraft {
|
|
|
|
|
fn into_cli(self, symbol: String) -> Cli {
|
|
|
|
|
Cli {
|
|
|
|
|
common: self.common,
|
|
|
|
|
symbol,
|
|
|
|
|
paths: self.paths,
|
|
|
|
|
languages: self.languages,
|
|
|
|
|
kinds: self.kinds,
|
|
|
|
|
limit: self.limit,
|
|
|
|
|
allow_empty: self.allow_empty,
|
|
|
|
|
parents: self.parents,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run(cli: &Cli) -> Result<ExitCode, CliError> {
|
|
|
|
|
let matches = collect_matches(cli)?;
|
|
|
|
|
|
|
|
|
|
match cli.common.render_mode() {
|
|
|
|
|
RenderMode::Json => print_json(&matches)?,
|
|
|
|
|
RenderMode::Toon => print_structured(&matches, RenderMode::Toon)?,
|
|
|
|
|
RenderMode::Text => {
|
|
|
|
|
if matches.is_empty() {
|
|
|
|
|
if !cli.common.quiet {
|
|
|
|
|
if cli.allow_empty {
|
|
|
|
|
println!("0 matches");
|
|
|
|
|
} else {
|
|
|
|
|
println!("0 matches (use --allow-empty to exit 0)");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
print!("{}", render_text(&matches, cli.parents, !cli.common.quiet));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(if matches.is_empty() {
|
|
|
|
|
if cli.allow_empty {
|
|
|
|
|
ExitCode::Success
|
|
|
|
|
} else {
|
|
|
|
|
ExitCode::NoResults
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ExitCode::Success
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_matches(cli: &Cli) -> Result<Vec<IndexedSymbol>, CliError> {
|
|
|
|
|
let roots = collect_roots(cli)?;
|
|
|
|
|
let files = discover_supported_files(&roots)?;
|
|
|
|
|
let mut indexer = CodeIndexer::new();
|
|
|
|
|
let mut matches = Vec::new();
|
|
|
|
|
|
|
|
|
|
for path in files {
|
|
|
|
|
let source = fs::read_to_string(&path).map_err(|error| {
|
|
|
|
|
CliError::runtime(format!("failed to read {}: {error}", path.display()))
|
|
|
|
|
})?;
|
|
|
|
|
let symbols = indexer.index_source(&path, &source)?;
|
|
|
|
|
for symbol in symbols {
|
|
|
|
|
if !matches_symbol(&symbol, cli) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
matches.push(symbol);
|
|
|
|
|
if matches.len() >= cli.limit {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if matches.len() >= cli.limit {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(matches)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parser_value_string(parser: &mut lexopt::Parser, flag: &str) -> Result<String, CliError> {
|
|
|
|
|
let value = parser
|
|
|
|
|
.value()
|
|
|
|
|
.map_err(|error| CliError::usage(error.to_string()))?;
|
|
|
|
|
os_value_string(value, flag)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn os_value_string(value: OsString, flag: &str) -> Result<String, CliError> {
|
|
|
|
|
value.into_string().map_err(|invalid| {
|
|
|
|
|
CliError::usage(format!(
|
|
|
|
|
"{flag} expects UTF-8 text, got '{}'",
|
|
|
|
|
invalid.to_string_lossy()
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_usize_flag(flag: &str, value: &str) -> Result<usize, CliError> {
|
|
|
|
|
value
|
|
|
|
|
.parse::<usize>()
|
|
|
|
|
.map_err(|error| CliError::usage(format!("invalid {flag} value '{value}': {error}")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_positive_usize_flag(flag: &str, value: &str) -> Result<usize, CliError> {
|
|
|
|
|
let parsed = parse_usize_flag(flag, value)?;
|
|
|
|
|
if parsed == 0 {
|
|
|
|
|
return Err(CliError::usage(format!("{flag} must be greater than 0")));
|
|
|
|
|
}
|
|
|
|
|
Ok(parsed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_language_list(value: &str) -> Result<BTreeSet<CodeLanguage>, CliError> {
|
|
|
|
|
let mut languages = BTreeSet::new();
|
|
|
|
|
for raw in split_csv_values(value) {
|
|
|
|
|
let language = parse_language_label(raw).ok_or_else(|| {
|
|
|
|
|
CliError::usage(format!(
|
|
|
|
|
"invalid --lang entry '{raw}'; expected {SUPPORTED_LANGUAGE_LIST}"
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
let _ = languages.insert(language);
|
|
|
|
|
}
|
|
|
|
|
Ok(languages)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_kind_list(value: &str) -> Result<BTreeSet<SymbolKind>, CliError> {
|
|
|
|
|
let mut kinds = BTreeSet::new();
|
|
|
|
|
for raw in split_csv_values(value) {
|
|
|
|
|
let kind = match raw {
|
|
|
|
|
"module" => SymbolKind::Module,
|
|
|
|
|
"namespace" => SymbolKind::Namespace,
|
|
|
|
|
"class" => SymbolKind::Class,
|
|
|
|
|
"struct" => SymbolKind::Struct,
|
|
|
|
|
"enum" => SymbolKind::Enum,
|
|
|
|
|
"interface" => SymbolKind::Interface,
|
|
|
|
|
"record" => SymbolKind::Record,
|
|
|
|
|
"trait" => SymbolKind::Trait,
|
|
|
|
|
"impl" => SymbolKind::Impl,
|
|
|
|
|
"type_alias" => SymbolKind::TypeAlias,
|
|
|
|
|
"function" => SymbolKind::Function,
|
|
|
|
|
"method" => SymbolKind::Method,
|
|
|
|
|
"constructor" => SymbolKind::Constructor,
|
|
|
|
|
"const" => SymbolKind::Const,
|
|
|
|
|
"static" => SymbolKind::Static,
|
|
|
|
|
other => {
|
|
|
|
|
return Err(CliError::usage(format!(
|
|
|
|
|
"invalid --kind entry '{other}'; expected module,namespace,class,struct,enum,interface,record,trait,impl,type_alias,function,method,constructor,const,static"
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let _ = kinds.insert(kind);
|
|
|
|
|
}
|
|
|
|
|
Ok(kinds)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn split_csv_values(value: &str) -> impl Iterator<Item = &str> {
|
|
|
|
|
value
|
|
|
|
|
.split(',')
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|part| !part.is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn collect_roots(cli: &Cli) -> Result<Vec<PathBuf>, CliError> {
|
|
|
|
|
if should_read_stdin(!cli.paths.is_empty(), cli.common.stdin_is_terminal()) {
|
|
|
|
|
let mut buffer = String::new();
|
|
|
|
|
io::stdin()
|
|
|
|
|
.read_to_string(&mut buffer)
|
|
|
|
|
.map_err(|error| CliError::runtime(format!("failed to read stdin: {error}")))?;
|
|
|
|
|
let roots = parse_paths_from_string(&buffer, cli.common.input_format)?;
|
|
|
|
|
if !roots.is_empty() {
|
|
|
|
|
return Ok(roots);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if cli.paths.is_empty() {
|
|
|
|
|
Ok(vec![PathBuf::from(".")])
|
|
|
|
|
} else {
|
|
|
|
|
common::expand_input_patterns(&cli.paths, "defsnip")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parse_paths_from_string(
|
|
|
|
|
buffer: &str,
|
|
|
|
|
input_format: InputFormat,
|
|
|
|
|
) -> Result<Vec<PathBuf>, CliError> {
|
|
|
|
|
common::read_existing_stdin_path_records(buffer, input_format, "defsnip")?
|
|
|
|
|
.map_or_else(|| Ok(Vec::new()), Ok)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn discover_supported_files(roots: &[PathBuf]) -> Result<Vec<PathBuf>, CliError> {
|
|
|
|
|
collect_matching_files(roots, &|path| detect_language(path).is_some())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn matches_symbol(symbol: &IndexedSymbol, cli: &Cli) -> bool {
|
|
|
|
|
if symbol.name != cli.symbol {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if let Some(languages) = &cli.languages {
|
|
|
|
|
if !languages.contains(&symbol.language) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(kinds) = &cli.kinds {
|
|
|
|
|
if !kinds.contains(&symbol.kind) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render_text(matches: &[IndexedSymbol], include_parents: bool, include_guidance: bool) -> String {
|
|
|
|
|
let mut output = String::new();
|
|
|
|
|
if include_guidance && matches.len() > 1 {
|
|
|
|
|
let _ = writeln!(
|
|
|
|
|
output,
|
|
|
|
|
"matches={} narrow_with=path|--kind|--lang",
|
|
|
|
|
matches.len()
|
|
|
|
|
);
|
|
|
|
|
let _ = writeln!(output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (index, symbol) in matches.iter().enumerate() {
|
|
|
|
|
if index > 0 {
|
|
|
|
|
let _ = writeln!(output);
|
|
|
|
|
}
|
|
|
|
|
let _ = writeln!(
|
|
|
|
|
output,
|
|
|
|
|
"== {}:{}-{} | {} {}",
|
|
|
|
|
symbol.path,
|
|
|
|
|
symbol.start_line,
|
|
|
|
|
symbol.end_line,
|
|
|
|
|
language_label(symbol.language),
|
|
|
|
|
kind_label(symbol.kind),
|
|
|
|
|
);
|
|
|
|
|
let _ = writeln!(output, "qualified: {}", symbol.qualified_name);
|
|
|
|
|
if include_parents && !symbol.parents.is_empty() {
|
|
|
|
|
let _ = writeln!(output, "parents: {}", symbol.parents.join("::"));
|
|
|
|
|
}
|
|
|
|
|
let _ = writeln!(output, "{}", symbol.text);
|
|
|
|
|
}
|
|
|
|
|
output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fn language_label(language: CodeLanguage) -> &'static str {
|
|
|
|
|
match language {
|
|
|
|
|
CodeLanguage::Rust
|
|
|
|
|
| CodeLanguage::Csharp
|
|
|
|
|
| CodeLanguage::Powershell
|
|
|
|
|
| CodeLanguage::Python
|
|
|
|
|
| CodeLanguage::Go
|
|
|
|
|
| CodeLanguage::Java
|
|
|
|
|
| CodeLanguage::Javascript
|
|
|
|
|
| CodeLanguage::Typescript => language.label(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fn kind_label(kind: SymbolKind) -> &'static str {
|
|
|
|
|
match kind {
|
|
|
|
|
SymbolKind::Module => "module",
|
|
|
|
|
SymbolKind::Namespace => "namespace",
|
|
|
|
|
SymbolKind::Class => "class",
|
|
|
|
|
SymbolKind::Struct => "struct",
|
|
|
|
|
SymbolKind::Enum => "enum",
|
|
|
|
|
SymbolKind::Interface => "interface",
|
|
|
|
|
SymbolKind::Record => "record",
|
|
|
|
|
SymbolKind::Trait => "trait",
|
|
|
|
|
SymbolKind::Impl => "impl",
|
|
|
|
|
SymbolKind::TypeAlias => "type_alias",
|
|
|
|
|
SymbolKind::Function => "function",
|
|
|
|
|
SymbolKind::Method => "method",
|
|
|
|
|
SymbolKind::Constructor => "constructor",
|
|
|
|
|
SymbolKind::Const => "const",
|
|
|
|
|
SymbolKind::Static => "static",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use common::ColorChoice;
|
|
|
|
|
use std::fs;
|
|
|
|
|
use tempfile::tempdir;
|
|
|
|
|
|
|
|
|
|
fn fixture_repo() -> PathBuf {
|
|
|
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
|
|
|
.join("..")
|
|
|
|
|
.join("..")
|
|
|
|
|
.join("fixtures")
|
|
|
|
|
.join("polyglot")
|
|
|
|
|
.join("repo")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn sample_symbol() -> IndexedSymbol {
|
|
|
|
|
IndexedSymbol {
|
|
|
|
|
engine: codeindex::ENGINE_NAME,
|
|
|
|
|
path: "fixture.rs".to_string(),
|
|
|
|
|
language: CodeLanguage::Rust,
|
|
|
|
|
kind: SymbolKind::Function,
|
|
|
|
|
name: "helper".to_string(),
|
|
|
|
|
qualified_name: "nested::Widget::helper".to_string(),
|
|
|
|
|
signature: "pub fn helper()".to_string(),
|
|
|
|
|
parents: vec!["nested".to_string(), "Widget".to_string()],
|
|
|
|
|
depth: 2,
|
|
|
|
|
start_line: 3,
|
|
|
|
|
end_line: 6,
|
|
|
|
|
text: "pub fn helper() {\n 42\n}".to_string(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cli_for(symbol: &str) -> Cli {
|
|
|
|
|
Cli {
|
|
|
|
|
common: CommonArgs {
|
|
|
|
|
json: false,
|
|
|
|
|
format: None,
|
|
|
|
|
input_format: InputFormat::Auto,
|
|
|
|
|
color: ColorChoice::Never,
|
|
|
|
|
quiet: false,
|
|
|
|
|
},
|
|
|
|
|
symbol: symbol.to_string(),
|
|
|
|
|
paths: vec![fixture_repo()],
|
|
|
|
|
languages: None,
|
|
|
|
|
kinds: None,
|
|
|
|
|
limit: 20,
|
|
|
|
|
allow_empty: false,
|
|
|
|
|
parents: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn parse_cli_and_filters_cover_help_version_and_validation() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_cli_from(["defsnip", "--help"]).expect("help").0,
|
|
|
|
|
ParseOutcome::Help
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
parse_cli_from(["defsnip", "--version"]).expect("version").0,
|
|
|
|
|
ParseOutcome::Version
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let (_, cli) = parse_cli_from([
|
|
|
|
|
"defsnip",
|
|
|
|
|
"--json",
|
|
|
|
|
"--input-format",
|
|
|
|
|
"jsonl",
|
|
|
|
|
"--color",
|
|
|
|
|
"never",
|
|
|
|
|
"--lang",
|
|
|
|
|
"rust,typescript",
|
|
|
|
|
"--kind",
|
|
|
|
|
"function,method",
|
|
|
|
|
"--limit",
|
|
|
|
|
"5",
|
|
|
|
|
"--parents",
|
|
|
|
|
"helper",
|
|
|
|
|
"fixtures/polyglot/repo",
|
|
|
|
|
])
|
|
|
|
|
.expect("parsed cli");
|
|
|
|
|
assert!(cli.common.json);
|
|
|
|
|
assert_eq!(cli.common.input_format, InputFormat::Jsonl);
|
|
|
|
|
assert_eq!(cli.common.color, ColorChoice::Never);
|
|
|
|
|
assert_eq!(cli.limit, 5);
|
|
|
|
|
assert!(cli.parents);
|
|
|
|
|
assert!(
|
|
|
|
|
cli.languages
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("languages")
|
|
|
|
|
.contains(&CodeLanguage::Rust)
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
cli.kinds
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("kinds")
|
|
|
|
|
.contains(&SymbolKind::Method)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
assert!(parse_cli_from(["defsnip"]).is_err());
|
|
|
|
|
assert!(parse_language_list("lua").is_err());
|
|
|
|
|
assert!(parse_kind_list("macro").is_err());
|
|
|
|
|
assert!(parse_usize_flag("--limit", "nope").is_err());
|
|
|
|
|
assert!(parse_positive_usize_flag("--limit", "0").is_err());
|
|
|
|
|
assert!(os_value_string(OsString::from("helper"), "symbol").is_ok());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn path_parsing_and_discovery_cover_auto_jsonl_and_missing_paths() {
|
|
|
|
|
let temp = tempdir().expect("tempdir");
|
|
|
|
|
let first = temp.path().join("alpha.rs");
|
|
|
|
|
let second = temp.path().join("beta.ts");
|
|
|
|
|
let third = temp.path().join("plain.rs");
|
|
|
|
|
let fourth = temp.path().join("code.py");
|
|
|
|
|
fs::write(&first, "a").expect("first");
|
|
|
|
|
fs::write(&second, "b").expect("second");
|
|
|
|
|
fs::write(&third, "c").expect("third");
|
|
|
|
|
fs::write(&fourth, "d").expect("fourth");
|
|
|
|
|
let line_paths = parse_paths_from_string(
|
|
|
|
|
&format!("{}\n{}\n", first.display(), second.display()),
|
|
|
|
|
InputFormat::Lines,
|
|
|
|
|
)
|
|
|
|
|
.expect("line paths");
|
|
|
|
|
assert_eq!(line_paths, vec![first, second]);
|
|
|
|
|
|
|
|
|
|
let json_paths = parse_paths_from_string(
|
|
|
|
|
&format!(
|
|
|
|
|
"{{\"path\":{}}}\n",
|
|
|
|
|
serde_json::to_string(&third.display().to_string()).expect("json path")
|
|
|
|
|
),
|
|
|
|
|
InputFormat::Jsonl,
|
|
|
|
|
)
|
|
|
|
|
.expect("json paths");
|
|
|
|
|
assert_eq!(json_paths, vec![third.clone()]);
|
|
|
|
|
|
|
|
|
|
let auto_paths = parse_paths_from_string(
|
|
|
|
|
&format!("{}\n{}\n", third.display(), fourth.display()),
|
|
|
|
|
InputFormat::Auto,
|
|
|
|
|
)
|
|
|
|
|
.expect("auto paths");
|
|
|
|
|
assert_eq!(auto_paths, vec![fourth, third]);
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
discover_supported_files(&[fixture_repo()])
|
|
|
|
|
.expect("discover")
|
|
|
|
|
.len()
|
|
|
|
|
>= 8
|
|
|
|
|
);
|
|
|
|
|
assert!(discover_supported_files(&[fixture_repo().join("missing")]).is_err());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn collect_matches_render_and_run_cover_success_and_no_results() {
|
|
|
|
|
let mut cli = cli_for("helper");
|
|
|
|
|
let matches = collect_matches(&cli).expect("matches");
|
|
|
|
|
assert!(matches.len() >= 4);
|
|
|
|
|
assert!(matches.iter().all(|symbol| symbol.name == "helper"));
|
|
|
|
|
assert!(
|
|
|
|
|
matches
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|symbol| symbol.engine == codeindex::ENGINE_NAME)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
cli.languages = Some(BTreeSet::from([CodeLanguage::Typescript]));
|
|
|
|
|
cli.kinds = Some(BTreeSet::from([SymbolKind::Function]));
|
|
|
|
|
cli.limit = 2;
|
|
|
|
|
let filtered = collect_matches(&cli).expect("filtered");
|
|
|
|
|
assert!(!filtered.is_empty());
|
|
|
|
|
assert!(filtered.len() <= 2);
|
|
|
|
|
assert!(
|
|
|
|
|
filtered
|
|
|
|
|
.iter()
|
|
|
|
|
.all(|symbol| symbol.language == CodeLanguage::Typescript)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let text = render_text(&[sample_symbol()], true, true);
|
|
|
|
|
assert!(text.contains("qualified: nested::Widget::helper"));
|
|
|
|
|
assert!(text.contains("parents: nested::Widget"));
|
|
|
|
|
assert!(text.contains("pub fn helper()"));
|
|
|
|
|
assert_eq!(language_label(CodeLanguage::Powershell), "powershell");
|
|
|
|
|
assert_eq!(language_label(CodeLanguage::Java), "java");
|
|
|
|
|
assert_eq!(kind_label(SymbolKind::Constructor), "constructor");
|
|
|
|
|
|
|
|
|
|
let ambiguous_text = render_text(&[sample_symbol(), sample_symbol()], false, true);
|
|
|
|
|
assert!(ambiguous_text.contains("matches=2"));
|
|
|
|
|
assert!(ambiguous_text.contains("narrow_with=path|--kind|--lang"));
|
|
|
|
|
|
|
|
|
|
let match_cli = cli_for("helper");
|
|
|
|
|
assert_eq!(run(&match_cli).expect("run success"), ExitCode::Success);
|
|
|
|
|
|
|
|
|
|
let missing_cli = cli_for("does_not_exist");
|
|
|
|
|
assert_eq!(run(&missing_cli).expect("run empty"), ExitCode::NoResults);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn symbol_matching_honors_language_and_kind_filters() {
|
|
|
|
|
let symbol = sample_symbol();
|
|
|
|
|
let mut cli = cli_for("helper");
|
|
|
|
|
assert!(matches_symbol(&symbol, &cli));
|
|
|
|
|
|
|
|
|
|
cli.languages = Some(BTreeSet::from([CodeLanguage::Rust]));
|
|
|
|
|
assert!(matches_symbol(&symbol, &cli));
|
|
|
|
|
|
|
|
|
|
cli.languages = Some(BTreeSet::from([CodeLanguage::Python]));
|
|
|
|
|
assert!(!matches_symbol(&symbol, &cli));
|
|
|
|
|
|
|
|
|
|
cli.languages = None;
|
|
|
|
|
cli.kinds = Some(BTreeSet::from([SymbolKind::Method]));
|
|
|
|
|
assert!(!matches_symbol(&symbol, &cli));
|
|
|
|
|
}
|
|
|
|
|
}
|