chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,700 @@
|
||||
//! The `codeshape` command emits recursive AST-backed project maps.
|
||||
|
||||
use codeindex::{
|
||||
CodeIndexer, CodeLanguage, ENGINE_NAME, 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 serde::Serialize;
|
||||
use std::collections::BTreeSet;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::io::{self, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const MAX_SOURCE_BYTES: u64 = 8 * 1024 * 1024;
|
||||
|
||||
const HELP: &str = "\
|
||||
Emit recursive AST-backed codebase maps with compact signatures via the shared codeindex engine.
|
||||
|
||||
Usage:
|
||||
codeshape [OPTIONS] [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
|
||||
--max-files <COUNT> Maximum number of files to include
|
||||
--max-depth <COUNT> Maximum symbol depth to include
|
||||
--limit-per-file <COUNT> Maximum symbols to include per file after depth filtering
|
||||
-h, --help Show this help text
|
||||
-V, --version Show the command version
|
||||
|
||||
Examples:
|
||||
codeshape .\\fixtures\\polyglot\\repo
|
||||
codeshape --max-depth 1 --limit-per-file 8 . --json | ConvertFrom-Json
|
||||
'.\\fixtures\\polyglot\\repo' | codeshape --json | ConvertFrom-Json
|
||||
|
||||
JSON fields:
|
||||
engine, roots, files[].path, files[].language, files[].items[], totals
|
||||
";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Cli {
|
||||
common: CommonArgs,
|
||||
paths: Vec<PathBuf>,
|
||||
languages: Option<BTreeSet<CodeLanguage>>,
|
||||
max_files: usize,
|
||||
max_depth: Option<usize>,
|
||||
limit_per_file: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum ParseOutcome {
|
||||
Help,
|
||||
Version,
|
||||
Run,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct CodeShapeItem {
|
||||
kind: SymbolKind,
|
||||
name: String,
|
||||
qualified_name: String,
|
||||
signature: String,
|
||||
depth: usize,
|
||||
start_line: usize,
|
||||
end_line: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct FileReport {
|
||||
path: String,
|
||||
language: CodeLanguage,
|
||||
items: Vec<CodeShapeItem>,
|
||||
omitted_items: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct Totals {
|
||||
files_seen: usize,
|
||||
files_indexed: usize,
|
||||
files_omitted_by_limit: usize,
|
||||
symbols_emitted: usize,
|
||||
symbols_omitted: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
struct CodeShapeReport {
|
||||
engine: &'static str,
|
||||
roots: Vec<String>,
|
||||
files: Vec<FileReport>,
|
||||
totals: Totals,
|
||||
}
|
||||
|
||||
/// 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!("codeshape {}", 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "single-pass CLI parsing keeps global and shared output flags auditable"
|
||||
)]
|
||||
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 common = CommonArgs::default();
|
||||
let mut paths = Vec::new();
|
||||
let mut languages = None::<BTreeSet<CodeLanguage>>;
|
||||
let mut max_files = 200_usize;
|
||||
let mut max_depth = None::<usize>;
|
||||
let mut limit_per_file = 32_usize;
|
||||
|
||||
while let Some(argument) = parser
|
||||
.next()
|
||||
.map_err(|error| CliError::usage(error.to_string()))?
|
||||
{
|
||||
match argument {
|
||||
Long("help") | Short('h') => {
|
||||
return Ok((
|
||||
ParseOutcome::Help,
|
||||
Cli {
|
||||
common,
|
||||
paths,
|
||||
languages,
|
||||
max_files,
|
||||
max_depth,
|
||||
limit_per_file,
|
||||
},
|
||||
));
|
||||
}
|
||||
Long("version") | Short('V') => {
|
||||
return Ok((
|
||||
ParseOutcome::Version,
|
||||
Cli {
|
||||
common,
|
||||
paths,
|
||||
languages,
|
||||
max_files,
|
||||
max_depth,
|
||||
limit_per_file,
|
||||
},
|
||||
));
|
||||
}
|
||||
Long("json") => common.set_render_mode(RenderMode::Json),
|
||||
Long("toon") => common.set_render_mode(RenderMode::Toon),
|
||||
Long("format") => {
|
||||
let value = parser_value_string(&mut parser, "--format")?;
|
||||
common.set_render_mode(parse_format_choice(&value)?);
|
||||
}
|
||||
Long("input-format") => {
|
||||
common.input_format =
|
||||
parse_input_format(&parser_value_string(&mut parser, "--input-format")?)?;
|
||||
}
|
||||
Long("color") => {
|
||||
common.color = parse_color_choice(&parser_value_string(&mut parser, "--color")?)?;
|
||||
}
|
||||
Long("quiet") => common.quiet = true,
|
||||
Long("lang") => {
|
||||
languages = Some(parse_language_list(&parser_value_string(
|
||||
&mut parser,
|
||||
"--lang",
|
||||
)?)?);
|
||||
}
|
||||
Long("max-files") => {
|
||||
max_files = parse_usize_flag(
|
||||
"--max-files",
|
||||
&parser_value_string(&mut parser, "--max-files")?,
|
||||
)?;
|
||||
}
|
||||
Long("max-depth") => {
|
||||
max_depth = Some(parse_usize_flag(
|
||||
"--max-depth",
|
||||
&parser_value_string(&mut parser, "--max-depth")?,
|
||||
)?);
|
||||
}
|
||||
Long("limit-per-file") => {
|
||||
limit_per_file = parse_usize_flag(
|
||||
"--limit-per-file",
|
||||
&parser_value_string(&mut parser, "--limit-per-file")?,
|
||||
)?;
|
||||
}
|
||||
ArgValue(value) => paths.push(PathBuf::from(value)),
|
||||
_ => {
|
||||
return Err(CliError::usage(
|
||||
"unsupported argument; use --help to see available options",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if max_files == 0 {
|
||||
return Err(CliError::usage("--max-files must be greater than 0"));
|
||||
}
|
||||
if limit_per_file == 0 {
|
||||
return Err(CliError::usage("--limit-per-file must be greater than 0"));
|
||||
}
|
||||
|
||||
Ok((
|
||||
ParseOutcome::Run,
|
||||
Cli {
|
||||
common,
|
||||
paths,
|
||||
languages,
|
||||
max_files,
|
||||
max_depth,
|
||||
limit_per_file,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn run(cli: &Cli) -> Result<ExitCode, CliError> {
|
||||
let report = build_report(cli)?;
|
||||
match cli.common.render_mode() {
|
||||
RenderMode::Json => print_json(&report)?,
|
||||
RenderMode::Toon => print_structured(&report, RenderMode::Toon)?,
|
||||
RenderMode::Text => print!("{}", render_text(&report)),
|
||||
}
|
||||
|
||||
Ok(if report.files.is_empty() {
|
||||
ExitCode::NoResults
|
||||
} else {
|
||||
ExitCode::Success
|
||||
})
|
||||
}
|
||||
|
||||
fn build_report(cli: &Cli) -> Result<CodeShapeReport, CliError> {
|
||||
let roots = collect_roots(cli)?;
|
||||
let discovered = discover_supported_files(&roots)?;
|
||||
let mut indexer = CodeIndexer::new();
|
||||
let mut files = Vec::new();
|
||||
let mut totals = Totals {
|
||||
files_seen: discovered.len(),
|
||||
files_indexed: 0,
|
||||
files_omitted_by_limit: 0,
|
||||
symbols_emitted: 0,
|
||||
symbols_omitted: 0,
|
||||
};
|
||||
|
||||
for path in discovered {
|
||||
if files.len() >= cli.max_files {
|
||||
totals.files_omitted_by_limit += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(language) = detect_language(&path) else {
|
||||
continue;
|
||||
};
|
||||
if cli
|
||||
.languages
|
||||
.as_ref()
|
||||
.is_some_and(|languages| !languages.contains(&language))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_source_too_large(&path, MAX_SOURCE_BYTES)? {
|
||||
totals.files_omitted_by_limit += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let source = fs::read_to_string(&path).map_err(|error| {
|
||||
CliError::runtime(format!("failed to read {}: {error}", path.display()))
|
||||
})?;
|
||||
let mut items = indexer
|
||||
.index_source_summary(&path, &source)?
|
||||
.into_iter()
|
||||
.filter(|item| cli.max_depth.is_none_or(|depth| item.depth <= depth))
|
||||
.map(into_codeshape_item)
|
||||
.collect::<Vec<_>>();
|
||||
items.sort_unstable_by(|left, right| {
|
||||
(left.depth, left.start_line, left.name.as_str()).cmp(&(
|
||||
right.depth,
|
||||
right.start_line,
|
||||
right.name.as_str(),
|
||||
))
|
||||
});
|
||||
if items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let omitted_items = items.len().saturating_sub(cli.limit_per_file);
|
||||
items.truncate(cli.limit_per_file);
|
||||
totals.files_indexed += 1;
|
||||
totals.symbols_emitted += items.len();
|
||||
totals.symbols_omitted += omitted_items;
|
||||
files.push(FileReport {
|
||||
path: path.display().to_string(),
|
||||
language,
|
||||
items,
|
||||
omitted_items,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CodeShapeReport {
|
||||
engine: ENGINE_NAME,
|
||||
roots: roots
|
||||
.iter()
|
||||
.map(|path| path.display().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
files,
|
||||
totals,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_source_too_large(path: &Path, max_bytes: u64) -> Result<bool, CliError> {
|
||||
let metadata = fs::metadata(path).map_err(|error| {
|
||||
CliError::runtime(format!(
|
||||
"failed to read metadata for {}: {error}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
Ok(metadata.len() > max_bytes)
|
||||
}
|
||||
|
||||
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()))?;
|
||||
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_language_list(value: &str) -> Result<BTreeSet<CodeLanguage>, CliError> {
|
||||
let mut languages = BTreeSet::new();
|
||||
for raw in value
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|part| !part.is_empty())
|
||||
{
|
||||
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 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, "codeshape")
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_paths_from_string(
|
||||
buffer: &str,
|
||||
input_format: InputFormat,
|
||||
) -> Result<Vec<PathBuf>, CliError> {
|
||||
common::read_existing_stdin_path_records(buffer, input_format, "codeshape")?
|
||||
.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 into_codeshape_item(symbol: IndexedSymbol) -> CodeShapeItem {
|
||||
CodeShapeItem {
|
||||
kind: symbol.kind,
|
||||
name: symbol.name,
|
||||
qualified_name: symbol.qualified_name,
|
||||
signature: symbol.signature,
|
||||
depth: symbol.depth,
|
||||
start_line: symbol.start_line,
|
||||
end_line: symbol.end_line,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_text(report: &CodeShapeReport) -> String {
|
||||
let mut output = String::new();
|
||||
for file in &report.files {
|
||||
let _ = writeln!(output, "{} [{}]", file.path, language_label(file.language));
|
||||
for item in &file.items {
|
||||
let indent = " ".repeat(item.depth + 1);
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"{}{} {} :: {}",
|
||||
indent,
|
||||
kind_label(item.kind),
|
||||
item.qualified_name,
|
||||
item.signature,
|
||||
);
|
||||
}
|
||||
if file.omitted_items > 0 {
|
||||
let _ = writeln!(output, " ... {} more item(s)", file.omitted_items);
|
||||
}
|
||||
}
|
||||
let _ = writeln!(
|
||||
output,
|
||||
"totals: files_indexed={} symbols_emitted={} symbols_omitted={} files_omitted_by_limit={}",
|
||||
report.totals.files_indexed,
|
||||
report.totals.symbols_emitted,
|
||||
report.totals.symbols_omitted,
|
||||
report.totals.files_omitted_by_limit
|
||||
);
|
||||
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;
|
||||
|
||||
fn fixture_repo() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("fixtures")
|
||||
.join("polyglot")
|
||||
.join("repo")
|
||||
}
|
||||
|
||||
fn cli_for(root: PathBuf) -> Cli {
|
||||
Cli {
|
||||
common: CommonArgs {
|
||||
json: false,
|
||||
format: None,
|
||||
input_format: InputFormat::Auto,
|
||||
color: ColorChoice::Never,
|
||||
quiet: false,
|
||||
},
|
||||
paths: vec![root],
|
||||
languages: None,
|
||||
max_files: 200,
|
||||
max_depth: None,
|
||||
limit_per_file: 32,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_symbol() -> IndexedSymbol {
|
||||
IndexedSymbol {
|
||||
engine: ENGINE_NAME,
|
||||
path: "fixture.ts".to_string(),
|
||||
language: CodeLanguage::Typescript,
|
||||
kind: SymbolKind::Function,
|
||||
name: "helper".to_string(),
|
||||
qualified_name: "web::helper".to_string(),
|
||||
signature: "export const helper = (name: string) => name.trim();".to_string(),
|
||||
parents: Vec::new(),
|
||||
depth: 0,
|
||||
start_line: 1,
|
||||
end_line: 1,
|
||||
text: "export const helper = (name: string) => name.trim();".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cli_and_lists_cover_help_version_and_validation() {
|
||||
assert_eq!(
|
||||
parse_cli_from(["codeshape", "--help"]).expect("help").0,
|
||||
ParseOutcome::Help
|
||||
);
|
||||
assert_eq!(
|
||||
parse_cli_from(["codeshape", "--version"])
|
||||
.expect("version")
|
||||
.0,
|
||||
ParseOutcome::Version
|
||||
);
|
||||
|
||||
let (_, cli) = parse_cli_from([
|
||||
"codeshape",
|
||||
"--json",
|
||||
"--input-format",
|
||||
"jsonl",
|
||||
"--color",
|
||||
"never",
|
||||
"--lang",
|
||||
"rust,java,typescript",
|
||||
"--max-files",
|
||||
"4",
|
||||
"--max-depth",
|
||||
"1",
|
||||
"--limit-per-file",
|
||||
"6",
|
||||
"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.max_files, 4);
|
||||
assert_eq!(cli.max_depth, Some(1));
|
||||
assert_eq!(cli.limit_per_file, 6);
|
||||
assert!(
|
||||
cli.languages
|
||||
.as_ref()
|
||||
.expect("languages")
|
||||
.contains(&CodeLanguage::Typescript)
|
||||
&& cli
|
||||
.languages
|
||||
.as_ref()
|
||||
.expect("languages")
|
||||
.contains(&CodeLanguage::Java)
|
||||
);
|
||||
|
||||
assert!(parse_cli_from(["codeshape", "--max-files", "0"]).is_err());
|
||||
assert!(parse_cli_from(["codeshape", "--limit-per-file", "0"]).is_err());
|
||||
assert!(parse_language_list("lua").is_err());
|
||||
assert!(parse_usize_flag("--max-files", "nope").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_parsing_and_discovery_cover_supported_inputs_and_failures() {
|
||||
let source = PathBuf::from("src/lib.rs");
|
||||
let manifest = PathBuf::from("Cargo.toml");
|
||||
|
||||
let line_paths = parse_paths_from_string("src/lib.rs\nCargo.toml\n", InputFormat::Lines)
|
||||
.expect("line paths");
|
||||
assert_eq!(line_paths, vec![manifest.clone(), source.clone()]);
|
||||
|
||||
let json_paths = parse_paths_from_string("{\"path\":\"src/lib.rs\"}\n", InputFormat::Jsonl)
|
||||
.expect("json paths");
|
||||
assert_eq!(json_paths, vec![source.clone()]);
|
||||
|
||||
let auto_paths = parse_paths_from_string("src/lib.rs\nCargo.toml\n", InputFormat::Auto)
|
||||
.expect("auto paths");
|
||||
assert_eq!(auto_paths, vec![manifest, source]);
|
||||
|
||||
assert!(
|
||||
discover_supported_files(&[fixture_repo()])
|
||||
.expect("discover")
|
||||
.len()
|
||||
>= 8
|
||||
);
|
||||
assert!(discover_supported_files(&[fixture_repo().join("missing")]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_report_skips_supported_sources_above_size_limit() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"codeshape-large-source-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("clock")
|
||||
.as_nanos()
|
||||
));
|
||||
fs::create_dir_all(&root).expect("temp root");
|
||||
let large_len = usize::try_from(MAX_SOURCE_BYTES + 1).expect("test size fits usize");
|
||||
fs::write(root.join("large.rs"), vec![b' '; large_len]).expect("large source");
|
||||
|
||||
let report = build_report(&cli_for(root.clone())).expect("report");
|
||||
|
||||
assert!(report.files.is_empty());
|
||||
assert_eq!(report.totals.files_omitted_by_limit, 1);
|
||||
fs::remove_dir_all(root).expect("cleanup");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_report_render_and_run_cover_truncation_and_empty_results() {
|
||||
let mut cli = cli_for(fixture_repo());
|
||||
let report = build_report(&cli).expect("report");
|
||||
assert_eq!(report.engine, ENGINE_NAME);
|
||||
assert!(report.files.len() >= 6);
|
||||
assert!(report.totals.files_seen >= report.totals.files_indexed);
|
||||
assert!(
|
||||
report
|
||||
.files
|
||||
.iter()
|
||||
.any(|file| file.path.ends_with("web\\app.ts"))
|
||||
);
|
||||
|
||||
cli.languages = Some(BTreeSet::from([CodeLanguage::Rust]));
|
||||
cli.max_files = 1;
|
||||
cli.max_depth = Some(1);
|
||||
cli.limit_per_file = 2;
|
||||
let limited = build_report(&cli).expect("limited report");
|
||||
assert_eq!(limited.files.len(), 1);
|
||||
assert!(limited.files[0].omitted_items <= limited.totals.symbols_omitted);
|
||||
|
||||
let item = into_codeshape_item(sample_symbol());
|
||||
assert_eq!(item.name, "helper");
|
||||
let text = render_text(&CodeShapeReport {
|
||||
engine: ENGINE_NAME,
|
||||
roots: vec!["repo".to_string()],
|
||||
files: vec![FileReport {
|
||||
path: "repo/web/app.ts".to_string(),
|
||||
language: CodeLanguage::Typescript,
|
||||
items: vec![item],
|
||||
omitted_items: 2,
|
||||
}],
|
||||
totals: Totals {
|
||||
files_seen: 1,
|
||||
files_indexed: 1,
|
||||
files_omitted_by_limit: 0,
|
||||
symbols_emitted: 1,
|
||||
symbols_omitted: 2,
|
||||
},
|
||||
});
|
||||
assert!(text.contains("repo/web/app.ts [typescript]"));
|
||||
assert!(text.contains("function web::helper"));
|
||||
assert!(text.contains("... 2 more item(s)"));
|
||||
assert!(text.contains("totals: files_indexed=1"));
|
||||
assert_eq!(language_label(CodeLanguage::Javascript), "javascript");
|
||||
assert_eq!(language_label(CodeLanguage::Java), "java");
|
||||
assert_eq!(kind_label(SymbolKind::Static), "static");
|
||||
|
||||
let success_cli = cli_for(fixture_repo());
|
||||
assert_eq!(run(&success_cli).expect("run success"), ExitCode::Success);
|
||||
|
||||
let empty_cli = cli_for(
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("..")
|
||||
.join("..")
|
||||
.join("README.md"),
|
||||
);
|
||||
assert_eq!(run(&empty_cli).expect("run empty"), ExitCode::NoResults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `codeshape`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(codeshape::main_entry());
|
||||
}
|
||||
Reference in New Issue
Block a user