diff --git a/crates/codescythe/analyze.rs b/crates/codescythe/analyze.rs index 54c0837..6ab7e98 100644 --- a/crates/codescythe/analyze.rs +++ b/crates/codescythe/analyze.rs @@ -86,9 +86,10 @@ pub fn analyze_path( config: &CodescytheConfig, options: AnalysisOptions, ) -> Result { - let cwd = cwd - .canonicalize() - .with_context(|| format!("failed to resolve {}", cwd.display()))?; + let cwd = normalize_path(cwd); + if !cwd.exists() { + anyhow::bail!("analysis root does not exist: {}", cwd.display()); + } let project_files = discover_project_files(&cwd, config)?; let entry_files = discover_entry_files(&cwd, config, &project_files)?; let entry_set = entry_files.iter().cloned().collect::>(); diff --git a/crates/codescythe_cli/BUILD.bazel b/crates/codescythe_cli/BUILD.bazel index 8602813..738dc4d 100644 --- a/crates/codescythe_cli/BUILD.bazel +++ b/crates/codescythe_cli/BUILD.bazel @@ -1,4 +1,5 @@ load("@rules_rs//rs:rust_binary.bzl", "rust_binary") +load("@rules_rs//rs:rust_test.bzl", "rust_test") load( ":release_binary.bzl", "release_binary_darwin_arm64", @@ -20,6 +21,19 @@ rust_binary( ], ) +rust_test( + name = "codescythe_cli_test", + srcs = ["main.rs"], + crate_name = "codescythe_cli_test", + edition = "2024", + deps = [ + "//crates/codescythe", + "@crates//:anyhow", + "@crates//:clap", + "@crates//:serde_json", + ], +) + release_binary_linux_amd64( name = "release_binary_linux_amd64", srcs = [":codescythe"], diff --git a/crates/codescythe_cli/main.rs b/crates/codescythe_cli/main.rs index a24b5c3..d538bcc 100644 --- a/crates/codescythe_cli/main.rs +++ b/crates/codescythe_cli/main.rs @@ -1,4 +1,8 @@ -use std::{path::PathBuf, process::ExitCode}; +use std::{ + env, + path::{Path, PathBuf}, + process::ExitCode, +}; use anyhow::Result; use clap::Parser; @@ -9,8 +13,8 @@ struct Args { #[arg(short, long)] config: Option, - #[arg(short = 'C', long, default_value = ".")] - directory: PathBuf, + #[arg(short = 'C', long)] + directory: Option, #[arg(long)] fix: bool, @@ -40,8 +44,8 @@ fn main() -> ExitCode { fn run() -> Result { let args = Args::parse(); - let cwd = args.directory.canonicalize()?; let config = args.config.as_deref(); + let cwd = analysis_root(args.directory.as_deref(), config)?; if args.fix { let result = codescythe::run_and_fix(&cwd, config)?; @@ -119,3 +123,43 @@ fn print_text_report(analysis: &codescythe::Analysis) { } } } + +fn analysis_root(directory: Option<&Path>, config: Option<&Path>) -> Result { + if let Some(directory) = directory { + return Ok(directory.to_path_buf()); + } + + if let Some(parent) = config + .and_then(Path::parent) + .filter(|parent| !parent.as_os_str().is_empty()) + { + return Ok(parent.to_path_buf()); + } + + Ok(env::current_dir()?) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn derives_analysis_root_from_config_parent_without_directory() { + let root = Path::new("/tmp/runfiles/_main"); + let config = root.join("codescythe.json"); + + let analysis_root = analysis_root(None, Some(&config)).unwrap(); + + assert_eq!(analysis_root, root); + } + + #[test] + fn explicit_directory_overrides_config_parent() { + let directory = Path::new("/tmp/runfiles/_main"); + let config = Path::new("/tmp/runfiles/_main/pplx/frontend/codescythe.json"); + + let analysis_root = analysis_root(Some(directory), Some(config)).unwrap(); + + assert_eq!(analysis_root, directory); + } +} diff --git a/crates/codescythe_napi/lib.rs b/crates/codescythe_napi/lib.rs index a3a39aa..34ad95d 100644 --- a/crates/codescythe_napi/lib.rs +++ b/crates/codescythe_napi/lib.rs @@ -13,8 +13,8 @@ pub struct RunOptions { #[napi] pub fn analyze(options: Option) -> napi::Result { let options = options.unwrap_or_default(); - let cwd = cwd(options.cwd)?; let config = options.config.as_deref().map(PathBuf::from); + let cwd = cwd(options.cwd, config.as_deref())?; let analysis = codescythe::run(&cwd, config.as_deref()).map_err(to_napi_error)?; serde_json::to_string(&analysis).map_err(to_napi_error) } @@ -22,8 +22,8 @@ pub fn analyze(options: Option) -> napi::Result { #[napi] pub fn fix(options: Option) -> napi::Result { let options = options.unwrap_or_default(); - let cwd = cwd(options.cwd)?; let config = options.config.as_deref().map(PathBuf::from); + let cwd = cwd(options.cwd, config.as_deref())?; let result = codescythe::run_and_fix(&cwd, config.as_deref()).map_err(to_napi_error)?; serde_json::to_string(&result).map_err(to_napi_error) } @@ -38,10 +38,15 @@ impl Default for RunOptions { } } -fn cwd(value: Option) -> napi::Result { +fn cwd(value: Option, config: Option<&std::path::Path>) -> napi::Result { match value { Some(path) => Ok(PathBuf::from(path)), - None => std::env::current_dir().map_err(to_napi_error), + None => config + .and_then(std::path::Path::parent) + .filter(|parent| !parent.as_os_str().is_empty()) + .map(PathBuf::from) + .map(Ok) + .unwrap_or_else(|| std::env::current_dir().map_err(to_napi_error)), } } diff --git a/packages/codescythe/npm_smoke.ts b/packages/codescythe/npm_smoke.ts index 7734101..624017b 100644 --- a/packages/codescythe/npm_smoke.ts +++ b/packages/codescythe/npm_smoke.ts @@ -15,11 +15,11 @@ type Analysis = { }; type NativeBinding = { - analyze(options: {cwd: string}): string; + analyze(options: {config?: string; cwd?: string}): string; }; type Codescythe = { - analyze(options: {cwd: string}): Analysis; + analyze(options: {config?: string; cwd?: string}): Analysis; }; const repoRoot = process.cwd(); @@ -53,6 +53,12 @@ describe('@perplexity/codescythe npm package', () => { assertFixtureAnalysis(analysis); }); + it('uses the config parent as the cwd when cwd is omitted', () => { + const codescythe = smokeRequire('@perplexity/codescythe') as Codescythe; + const analysis = codescythe.analyze({config: path.join(fixture, 'codescythe.json')}); + assertFixtureAnalysis(analysis); + }); + it('runs the public package bin', () => { const binResult = childProcess.spawnSync( process.execPath, @@ -75,6 +81,29 @@ describe('@perplexity/codescythe npm package', () => { assert.equal(binResult.status, 1, binResult.stderr || binResult.stdout); assertFixtureAnalysis(JSON.parse(binResult.stdout) as Analysis); }); + + it('runs the public package bin from the config parent', () => { + const binResult = childProcess.spawnSync( + process.execPath, + [ + '--experimental-transform-types', + path.join(mainPackageDir, 'bin/codescythe.ts'), + '--json', + '--config', + path.join(fixture, 'codescythe.json'), + ], + { + encoding: 'utf8', + env: { + ...process.env, + NODE_PATH: nodeModules, + }, + }, + ); + + assert.equal(binResult.status, 1, binResult.stderr || binResult.stdout); + assertFixtureAnalysis(JSON.parse(binResult.stdout) as Analysis); + }); }); function packageDirFromEnv(name: string): string {