forked from Crockan/MercuryToolbox
chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "defsnip"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
readme.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Extract full code definitions by exact symbol name with AST-backed matching."
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codeindex = { path = "../codeindex" }
|
||||
common = { path = "../common", default-features = false }
|
||||
lexopt.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd.workspace = true
|
||||
predicates.workspace = true
|
||||
serde_json.workspace = true
|
||||
tempfile.workspace = true
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `defsnip`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(defsnip::main_entry());
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//! Integration tests for the `defsnip` command.
|
||||
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn cargo_command() -> Command {
|
||||
Command::cargo_bin("defsnip").expect("binary")
|
||||
}
|
||||
|
||||
fn fixture_path(relative: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("fixtures")
|
||||
.join("polyglot")
|
||||
.join("repo")
|
||||
.join(relative)
|
||||
}
|
||||
|
||||
fn pwsh_command(script: impl AsRef<str>) -> Command {
|
||||
let mut command = Command::new("pwsh");
|
||||
command
|
||||
.arg("-NoProfile")
|
||||
.arg("-Command")
|
||||
.arg(script.as_ref());
|
||||
command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_exact_symbol_matches_as_json() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.arg("--json")
|
||||
.arg("helper")
|
||||
.arg(fixture_path(""))
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"qualified_name\":\"helper\""))
|
||||
.stdout(predicate::str::contains(
|
||||
"\"qualified_name\":\"nested::Widget::helper\"",
|
||||
))
|
||||
.stdout(predicate::str::contains("\"qualified_name\":\"Worker::build\"").not());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_method_matches_and_renders_text() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.args(["--kind", "method", "--parents", "Build"])
|
||||
.arg(fixture_path(""))
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("RocketBuilder::Build"))
|
||||
.stdout(predicate::str::contains("NestedThing::Build"))
|
||||
.stdout(predicate::str::contains("public void Build(string name)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_powershell_pipeline_paths() {
|
||||
let binary = assert_cmd::cargo::cargo_bin("defsnip");
|
||||
let path = fixture_path("web/app.ts");
|
||||
let script = format!(
|
||||
"'{}' | & '{}' helper --json | ConvertFrom-Json | Select-Object -ExpandProperty qualified_name",
|
||||
path.display(),
|
||||
binary.display()
|
||||
);
|
||||
let mut command = pwsh_command(script);
|
||||
command
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("helper"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_includes_examples_and_pipeline_usage() {
|
||||
let mut command = cargo_command();
|
||||
command
|
||||
.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("--kind"))
|
||||
.stdout(predicate::str::contains("ConvertFrom-Json"))
|
||||
.stdout(predicate::str::contains("defsnip helper"));
|
||||
}
|
||||
Reference in New Issue
Block a user