From 8ab8f9b4e1ada28ab5a186a9928b4d0210cadf66 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 19 Jan 2026 23:50:41 -0300 Subject: [PATCH] feat: Auto-detect file type in `ast` command by extension --- crates/plotnik-cli/src/cli/commands.rs | 7 ++- crates/plotnik-cli/src/cli/dispatch.rs | 30 ++++++++++ crates/plotnik-cli/src/cli/dispatch_tests.rs | 59 ++++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/crates/plotnik-cli/src/cli/commands.rs b/crates/plotnik-cli/src/cli/commands.rs index bcede551..f7198be6 100644 --- a/crates/plotnik-cli/src/cli/commands.rs +++ b/crates/plotnik-cli/src/cli/commands.rs @@ -60,19 +60,20 @@ pub fn build_cli() -> Command { /// /// Accepts all runtime flags for unified CLI experience. /// Shows query AST when query is provided, source AST when source is provided. +/// Single positional file is auto-detected: .ptk → query, otherwise → source. pub fn ast_command() -> Command { let cmd = Command::new("ast") .about("Show AST of query and/or source file") .override_usage( "\ - plotnik ast [SOURCE] + plotnik ast # auto-detect by extension + plotnik ast # both ASTs plotnik ast -q [SOURCE] - plotnik ast plotnik ast -s -l ", ) .after_help( r#"EXAMPLES: - plotnik ast query.ptk # query AST + plotnik ast query.ptk # query AST (.ptk extension) plotnik ast app.ts # source AST (tree-sitter) plotnik ast query.ptk app.ts # both ASTs plotnik ast query.ptk app.ts --raw # CST / include anonymous nodes diff --git a/crates/plotnik-cli/src/cli/dispatch.rs b/crates/plotnik-cli/src/cli/dispatch.rs index 42e8cced..17111f36 100644 --- a/crates/plotnik-cli/src/cli/dispatch.rs +++ b/crates/plotnik-cli/src/cli/dispatch.rs @@ -39,6 +39,11 @@ impl AstParams { let (query_path, source_path) = shift_positional_to_source(query_text.is_some(), query_path, source_path); + // Extension-based detection: when a single file is provided without -q, + // .ptk → query, otherwise → source (detect language from extension). + let (query_path, source_path) = + detect_file_type_by_extension(query_path, source_path, query_text.is_some()); + Self { query_path, query_text, @@ -352,3 +357,28 @@ fn shift_positional_to_source( (query_path, source_path) } } + +/// Detect file type by extension for ast command. +/// When a single file is provided without -q: .ptk → query, otherwise → source. +fn detect_file_type_by_extension( + query_path: Option, + source_path: Option, + has_query_text: bool, +) -> (Option, Option) { + // Only apply when: single positional file, no -q flag, no explicit source + if has_query_text || source_path.is_some() { + return (query_path, source_path); + } + + let Some(path) = query_path else { + return (None, None); + }; + + // .ptk extension → treat as query + if path.extension().is_some_and(|ext| ext == "ptk") { + return (Some(path), None); + } + + // Any other extension → treat as source file + (None, Some(path)) +} diff --git a/crates/plotnik-cli/src/cli/dispatch_tests.rs b/crates/plotnik-cli/src/cli/dispatch_tests.rs index c7e48b03..115bcd6f 100644 --- a/crates/plotnik-cli/src/cli/dispatch_tests.rs +++ b/crates/plotnik-cli/src/cli/dispatch_tests.rs @@ -712,3 +712,62 @@ fn check_accepts_raw_flag() { result.err() ); } + +// Extension-based detection tests for ast command + +#[test] +fn ast_detects_ptk_as_query() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from(["ast", "query.ptk"]); + assert!(result.is_ok()); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + + // .ptk extension → treat as query + assert_eq!(params.query_path, Some(PathBuf::from("query.ptk"))); + assert_eq!(params.source_path, None); +} + +#[test] +fn ast_detects_non_ptk_as_source() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from(["ast", "app.js"]); + assert!(result.is_ok()); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + + // Non-.ptk extension → treat as source + assert_eq!(params.query_path, None); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +} + +#[test] +fn ast_no_extension_detection_with_two_positionals() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from(["ast", "query.ptk", "app.js"]); + assert!(result.is_ok()); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + + // Two positionals → first is query, second is source (no detection) + assert_eq!(params.query_path, Some(PathBuf::from("query.ptk"))); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +} + +#[test] +fn ast_no_extension_detection_with_inline_query() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from(["ast", "-q", "(id) @x", "app.js"]); + assert!(result.is_ok()); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + + // -q provided → positional shift takes precedence, no extension detection + assert_eq!(params.query_path, None); + assert_eq!(params.query_text, Some("(id) @x".to_string())); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +}