chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:41:59 +08:00
commit e365e5df4d
508 changed files with 163373 additions and 0 deletions
+26
View File
@@ -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
+672
View File
@@ -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));
}
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `defsnip`.
fn main() {
std::process::exit(defsnip::main_entry());
}
+85
View File
@@ -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"));
}