From 05191f93d5f7783154f1743b582aa00fa71f86b4 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 5 Jan 2026 21:35:05 -0300 Subject: [PATCH] feat: unify `tree` and query AST into `ast` command --- AGENTS.md | 16 +- crates/plotnik-cli/src/cli/args.rs | 8 - crates/plotnik-cli/src/cli/commands.rs | 62 ++++-- crates/plotnik-cli/src/cli/dispatch.rs | 33 +++- crates/plotnik-cli/src/cli/dispatch_tests.rs | 196 ++++++++++++++++++- crates/plotnik-cli/src/cli/mod.rs | 2 +- crates/plotnik-cli/src/commands/ast.rs | 189 ++++++++++++++++++ crates/plotnik-cli/src/commands/mod.rs | 2 +- crates/plotnik-cli/src/commands/tree.rs | 193 ------------------ crates/plotnik-cli/src/main.rs | 8 +- 10 files changed, 467 insertions(+), 242 deletions(-) create mode 100644 crates/plotnik-cli/src/commands/ast.rs delete mode 100644 crates/plotnik-cli/src/commands/tree.rs diff --git a/AGENTS.md b/AGENTS.md index 82c74b83..04682324 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -145,7 +145,7 @@ Tree-sitter: `((a) (b))` — Plotnik: `{(a) (b)}`. The #1 syntax error. ``` crates/ plotnik-cli/ # CLI tool - src/commands/ # Subcommands (check, dump, exec, infer, trace, tree, langs) + src/commands/ # Subcommands (ast, check, dump, exec, infer, trace, langs) plotnik-core/ # Common code (Interner, Symbol) plotnik-lib/ # Plotnik as library src/ @@ -172,7 +172,7 @@ Run: `cargo run -p plotnik-cli -- ` | Command | Purpose | | ------- | ----------------------------- | -| `tree` | Explore tree-sitter AST | +| `ast` | Show AST of query and/or source | | `check` | Validate query | | `dump` | Show compiled bytecode | | `infer` | Generate TypeScript types | @@ -180,14 +180,15 @@ Run: `cargo run -p plotnik-cli -- ` | `trace` | Trace execution for debugging | | `langs` | List supported languages | -## tree +## ast -Explore a source file's tree-sitter AST. +Show AST of query and/or source file. ```sh -cargo run -p plotnik-cli -- tree app.ts -cargo run -p plotnik-cli -- tree app.ts --raw -cargo run -p plotnik-cli -- tree app.ts --spans +cargo run -p plotnik-cli -- ast query.ptk # query AST +cargo run -p plotnik-cli -- ast app.ts # source AST (tree-sitter) +cargo run -p plotnik-cli -- ast query.ptk app.ts # both ASTs +cargo run -p plotnik-cli -- ast query.ptk app.ts --raw # CST / include anonymous nodes ``` ## check @@ -275,6 +276,7 @@ cargo run -p plotnik-cli -- langs - Early exit (`return`, `continue`, `break`) over deep nesting - Comments for seniors, not juniors - Rust 2024 `let` chains: `if let Some(x) = a && let Some(y) = b { ... }` +- Never claim "all tests pass" — CI verifies this ## Lifetime Conventions diff --git a/crates/plotnik-cli/src/cli/args.rs b/crates/plotnik-cli/src/cli/args.rs index 0fd0c335..93cc42c8 100644 --- a/crates/plotnik-cli/src/cli/args.rs +++ b/crates/plotnik-cli/src/cli/args.rs @@ -69,14 +69,6 @@ pub fn raw_arg() -> Arg { .help("Include anonymous nodes (literals, punctuation)") } -/// Show source positions (--spans). -pub fn spans_arg() -> Arg { - Arg::new("spans") - .long("spans") - .action(ArgAction::SetTrue) - .help("Show source positions") -} - /// Treat warnings as errors (--strict). pub fn strict_arg() -> Arg { Arg::new("strict") diff --git a/crates/plotnik-cli/src/cli/commands.rs b/crates/plotnik-cli/src/cli/commands.rs index 3a971cc7..0e1d504a 100644 --- a/crates/plotnik-cli/src/cli/commands.rs +++ b/crates/plotnik-cli/src/cli/commands.rs @@ -36,13 +36,18 @@ fn with_hidden_trace_args(cmd: Command) -> Command { .arg(fuel_arg().hide(true)) } +/// Add hidden AST args (for commands that don't show AST). +fn with_hidden_ast_args(cmd: Command) -> Command { + cmd.arg(raw_arg().hide(true)) +} + /// Build the complete CLI with all subcommands. pub fn build_cli() -> Command { Command::new("plotnik") .about("Query language for tree-sitter AST with type inference") .subcommand_required(true) .arg_required_else_help(true) - .subcommand(tree_command()) + .subcommand(ast_command()) .subcommand(check_command()) .subcommand(dump_command()) .subcommand(infer_command()) @@ -51,26 +56,39 @@ pub fn build_cli() -> Command { .subcommand(langs_command()) } -/// Explore a source file's tree-sitter AST. -pub fn tree_command() -> Command { - Command::new("tree") - .about("Explore a source file's tree-sitter AST") +/// Show AST of query and/or source file. +/// +/// Accepts all runtime flags for unified CLI experience. +/// Shows query AST when query is provided, source AST when source is provided. +pub fn ast_command() -> Command { + let cmd = Command::new("ast") + .about("Show AST of query and/or source file") .override_usage( "\ - plotnik tree - plotnik tree -s -l ", + plotnik ast [SOURCE] + plotnik ast -q [SOURCE] + plotnik ast + plotnik ast -s -l ", ) .after_help( r#"EXAMPLES: - plotnik tree app.ts # source file - plotnik tree app.ts --raw # include anonymous nodes - plotnik tree -s 'let x = 1' -l js # inline source"#, + plotnik ast query.ptk # query AST + 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 + plotnik ast -q '(id) @x' # inline query AST + plotnik ast -s 'let x = 1' -l js # inline source AST"#, ) + .arg(query_path_arg()) .arg(source_path_arg()) + .arg(query_text_arg()) .arg(source_text_arg()) .arg(lang_arg()) .arg(raw_arg()) - .arg(spans_arg()) + .arg(color_arg()); + + // Hidden unified flags + with_hidden_trace_args(with_hidden_exec_args(cmd)) } /// Validate a query. @@ -100,7 +118,9 @@ pub fn check_command() -> Command { .arg(color_arg()); // Hidden unified flags - with_hidden_trace_args(with_hidden_exec_args(with_hidden_source_args(cmd))) + with_hidden_ast_args(with_hidden_trace_args(with_hidden_exec_args( + with_hidden_source_args(cmd), + ))) } /// Show compiled bytecode. @@ -128,7 +148,9 @@ pub fn dump_command() -> Command { .arg(color_arg()); // Hidden unified flags - with_hidden_trace_args(with_hidden_exec_args(with_hidden_source_args(cmd))) + with_hidden_ast_args(with_hidden_trace_args(with_hidden_exec_args( + with_hidden_source_args(cmd), + ))) } /// Generate type definitions from a query. @@ -163,7 +185,9 @@ NOTE: Use --verbose-nodes to match `exec --verbose-nodes` output shape."#, .arg(color_arg()); // Hidden unified flags (use partial exec args since --verbose-nodes is visible) - with_hidden_trace_args(with_hidden_exec_args_partial(with_hidden_source_args(cmd))) + with_hidden_ast_args(with_hidden_trace_args(with_hidden_exec_args_partial( + with_hidden_source_args(cmd), + ))) } /// Execute a query against source code and output JSON. @@ -196,7 +220,7 @@ pub fn exec_command() -> Command { .arg(entry_arg()); // Hidden unified flags - with_hidden_trace_args(cmd) + with_hidden_ast_args(with_hidden_trace_args(cmd)) } /// Trace query execution for debugging. @@ -229,9 +253,11 @@ pub fn trace_command() -> Command { .arg(fuel_arg()); // Hidden unified flags (exec output flags only - entry is visible for trace) - cmd.arg(compact_arg().hide(true)) - .arg(verbose_nodes_arg().hide(true)) - .arg(check_arg().hide(true)) + with_hidden_ast_args( + cmd.arg(compact_arg().hide(true)) + .arg(verbose_nodes_arg().hide(true)) + .arg(check_arg().hide(true)), + ) } /// List supported languages. diff --git a/crates/plotnik-cli/src/cli/dispatch.rs b/crates/plotnik-cli/src/cli/dispatch.rs index 4fdceeec..5b6e7b44 100644 --- a/crates/plotnik-cli/src/cli/dispatch.rs +++ b/crates/plotnik-cli/src/cli/dispatch.rs @@ -11,41 +11,56 @@ use std::path::PathBuf; use clap::ArgMatches; use super::ColorChoice; +use crate::commands::ast::AstArgs; use crate::commands::check::CheckArgs; use crate::commands::dump::DumpArgs; use crate::commands::exec::ExecArgs; use crate::commands::infer::InferArgs; use crate::commands::trace::TraceArgs; -use crate::commands::tree::TreeArgs; -pub struct TreeParams { +pub struct AstParams { + pub query_path: Option, + pub query_text: Option, pub source_path: Option, pub source_text: Option, pub lang: Option, pub raw: bool, - pub spans: bool, + pub color: ColorChoice, } -impl TreeParams { +impl AstParams { pub fn from_matches(m: &ArgMatches) -> Self { + let query_path = m.get_one::("query_path").cloned(); + let query_text = m.get_one::("query_text").cloned(); + let source_path = m.get_one::("source_path").cloned(); + + // Positional shifting: when -q is used with a single positional, + // shift it from query_path to source_path. + let (query_path, source_path) = + shift_positional_to_source(query_text.is_some(), query_path, source_path); + Self { - source_path: m.get_one::("source_path").cloned(), + query_path, + query_text, + source_path, source_text: m.get_one::("source_text").cloned(), lang: m.get_one::("lang").cloned(), raw: m.get_flag("raw"), - spans: m.get_flag("spans"), + color: parse_color(m), } } } -impl From for TreeArgs { - fn from(p: TreeParams) -> Self { +impl From for AstArgs { + fn from(p: AstParams) -> Self { Self { + query_path: p.query_path, + query_text: p.query_text, source_path: p.source_path, source_text: p.source_text, lang: p.lang, raw: p.raw, - spans: p.spans, + color: p.color.should_colorize(), } } } diff --git a/crates/plotnik-cli/src/cli/dispatch_tests.rs b/crates/plotnik-cli/src/cli/dispatch_tests.rs index a1e07569..c7e48b03 100644 --- a/crates/plotnik-cli/src/cli/dispatch_tests.rs +++ b/crates/plotnik-cli/src/cli/dispatch_tests.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; use super::*; use crate::cli::commands::{ - check_command, dump_command, exec_command, infer_command, trace_command, + ast_command, check_command, dump_command, exec_command, infer_command, trace_command, }; #[test] @@ -518,3 +518,197 @@ fn dump_params_extracts_only_relevant_fields() { assert!(matches!(params.color, ColorChoice::Auto)); // No source_path, fuel, compact fields in DumpParams } + +// AST command tests + +#[test] +fn ast_accepts_exec_flags() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from([ + "ast", + "query.ptk", + "app.js", + "--compact", + "--check", + "--verbose-nodes", + "--entry", + "Foo", + ]); + assert!( + result.is_ok(), + "ast should accept exec flags: {:?}", + result.err() + ); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + assert_eq!(params.query_path, Some(PathBuf::from("query.ptk"))); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +} + +#[test] +fn ast_accepts_trace_flags() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from([ + "ast", + "query.ptk", + "app.js", + "--fuel", + "500", + "-vv", + "--no-result", + ]); + assert!( + result.is_ok(), + "ast should accept trace flags: {:?}", + result.err() + ); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + assert_eq!(params.query_path, Some(PathBuf::from("query.ptk"))); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +} + +#[test] +fn ast_shifts_positional_with_inline_query() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from(["ast", "-q", "(identifier) @id", "app.js"]); + assert!(result.is_ok()); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + + // With -q, the single positional should become source_path, not query_path + assert_eq!(params.query_path, None); + assert_eq!(params.query_text, Some("(identifier) @id".to_string())); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +} + +#[test] +fn ast_no_shift_with_both_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); + + // Without -q, both positionals are used as-is + assert_eq!(params.query_path, Some(PathBuf::from("query.ptk"))); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); +} + +#[test] +fn ast_help_shows_raw_flag() { + let mut cmd = ast_command(); + let help = cmd.render_help().to_string(); + + assert!(help.contains("--raw"), "ast help should show --raw"); +} + +#[test] +fn ast_help_hides_unified_flags() { + let mut cmd = ast_command(); + let help = cmd.render_help().to_string(); + + // Exec flags should be hidden + assert!( + !help.contains("--compact"), + "ast help should not show --compact" + ); + assert!( + !help.contains("--verbose-nodes"), + "ast help should not show --verbose-nodes" + ); + assert!( + !help.contains("--check"), + "ast help should not show --check" + ); + assert!( + !help.contains("--entry"), + "ast help should not show --entry" + ); + + // Trace flags should be hidden + assert!(!help.contains("--fuel"), "ast help should not show --fuel"); + assert!( + !help.contains("--no-result"), + "ast help should not show --no-result" + ); + assert!( + !help.contains("Verbosity level"), + "ast help should not show -v description" + ); +} + +#[test] +fn ast_params_extracts_all_fields() { + let cmd = ast_command(); + let result = cmd.try_get_matches_from([ + "ast", + "query.ptk", + "app.js", + "-l", + "typescript", + "--raw", + "--color", + "always", + ]); + assert!(result.is_ok()); + + let m = result.unwrap(); + let params = AstParams::from_matches(&m); + + assert_eq!(params.query_path, Some(PathBuf::from("query.ptk"))); + assert_eq!(params.source_path, Some(PathBuf::from("app.js"))); + assert_eq!(params.lang, Some("typescript".to_string())); + assert!(params.raw); + assert!(matches!(params.color, ColorChoice::Always)); +} + +// Test that other commands accept --raw (unified flag) + +#[test] +fn dump_accepts_raw_flag() { + let cmd = dump_command(); + let result = cmd.try_get_matches_from(["dump", "query.ptk", "--raw"]); + assert!( + result.is_ok(), + "dump should accept --raw flag: {:?}", + result.err() + ); +} + +#[test] +fn exec_accepts_raw_flag() { + let cmd = exec_command(); + let result = cmd.try_get_matches_from(["exec", "query.ptk", "app.js", "--raw"]); + assert!( + result.is_ok(), + "exec should accept --raw flag: {:?}", + result.err() + ); +} + +#[test] +fn trace_accepts_raw_flag() { + let cmd = trace_command(); + let result = cmd.try_get_matches_from(["trace", "query.ptk", "app.js", "--raw"]); + assert!( + result.is_ok(), + "trace should accept --raw flag: {:?}", + result.err() + ); +} + +#[test] +fn check_accepts_raw_flag() { + let cmd = check_command(); + let result = cmd.try_get_matches_from(["check", "query.ptk", "--raw"]); + assert!( + result.is_ok(), + "check should accept --raw flag: {:?}", + result.err() + ); +} diff --git a/crates/plotnik-cli/src/cli/mod.rs b/crates/plotnik-cli/src/cli/mod.rs index 7806a132..efe475fa 100644 --- a/crates/plotnik-cli/src/cli/mod.rs +++ b/crates/plotnik-cli/src/cli/mod.rs @@ -7,7 +7,7 @@ mod dispatch_tests; pub use commands::build_cli; pub use dispatch::{ - CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams, TreeParams, + AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams, }; /// Color output mode for CLI commands. diff --git a/crates/plotnik-cli/src/commands/ast.rs b/crates/plotnik-cli/src/commands/ast.rs new file mode 100644 index 00000000..247d32e2 --- /dev/null +++ b/crates/plotnik-cli/src/commands/ast.rs @@ -0,0 +1,189 @@ +//! Show AST of query and/or source file. + +use std::path::PathBuf; + +use arborium_tree_sitter as tree_sitter; +use plotnik_lib::QueryBuilder; + +use super::query_loader::load_query_source; +use super::run_common; + +pub struct AstArgs { + pub query_path: Option, + pub query_text: Option, + pub source_path: Option, + pub source_text: Option, + pub lang: Option, + pub raw: bool, + pub color: bool, +} + +pub fn run(args: AstArgs) { + let has_query = args.query_path.is_some() || args.query_text.is_some(); + let has_source = args.source_path.is_some() || args.source_text.is_some(); + + if !has_query && !has_source { + eprintln!("error: query or source required"); + std::process::exit(1); + } + + let show_headers = has_query && has_source; + + // Show Query AST if query provided + if has_query { + if show_headers { + println!("# Query AST"); + } + print_query_ast(&args); + } + + // Show Source AST if source provided + if has_source { + if show_headers { + println!("\n# Source AST"); + } + print_source_ast(&args); + } +} + +fn print_query_ast(args: &AstArgs) { + let source_map = match load_query_source(args.query_path.as_deref(), args.query_text.as_deref()) + { + Ok(map) => map, + Err(msg) => { + eprintln!("error: {}", msg); + std::process::exit(1); + } + }; + + if source_map.is_empty() { + eprintln!("error: query cannot be empty"); + std::process::exit(1); + } + + let query = match QueryBuilder::new(source_map).parse() { + Ok(parsed) => parsed.analyze(), + Err(e) => { + eprintln!("error: {}", e); + std::process::exit(1); + } + }; + + // Show diagnostics if any (warnings) + if query.diagnostics().has_errors() || query.diagnostics().has_warnings() { + eprint!( + "{}", + query + .diagnostics() + .render_colored(query.source_map(), args.color) + ); + } + + // Print AST (or CST if --raw) + let output = query.printer().raw(args.raw).with_trivia(args.raw).dump(); + print!("{}", output); +} + +fn print_source_ast(args: &AstArgs) { + let source = run_common::load_source( + args.source_text.as_deref(), + args.source_path.as_deref(), + args.query_path.as_deref(), + ); + let lang = run_common::resolve_lang(args.lang.as_deref(), args.source_path.as_deref()); + let tree = lang.parse(&source); + print!("{}", dump_tree(&tree, &source, args.raw)); +} + +fn dump_tree(tree: &tree_sitter::Tree, source: &str, raw: bool) -> String { + format_node(tree.root_node(), source, 0, raw) + "\n" +} + +fn format_node( + node: tree_sitter::Node, + source: &str, + depth: usize, + include_anonymous: bool, +) -> String { + format_node_with_field(node, None, source, depth, include_anonymous) +} + +fn format_node_with_field( + node: tree_sitter::Node, + field_name: Option<&str>, + source: &str, + depth: usize, + include_anonymous: bool, +) -> String { + if !include_anonymous && !node.is_named() { + return String::new(); + } + + let indent = " ".repeat(depth); + let kind = node.kind(); + let field_prefix = field_name.map(|f| format!("{}: ", f)).unwrap_or_default(); + + let children: Vec<_> = { + let mut cursor = node.walk(); + let mut result = Vec::new(); + if cursor.goto_first_child() { + loop { + let child = cursor.node(); + if include_anonymous || child.is_named() { + result.push((child, cursor.field_name())); + } + if !cursor.goto_next_sibling() { + break; + } + } + } + result + }; + + if children.is_empty() { + let text = node + .utf8_text(source.as_bytes()) + .unwrap_or(""); + return if text == kind { + format!("{}{}(\"{}\")", indent, field_prefix, escape_string(kind)) + } else { + format!( + "{}{}({} \"{}\")", + indent, + field_prefix, + kind, + escape_string(text) + ) + }; + } + + let mut out = format!("{}{}({}", indent, field_prefix, kind); + for (child, child_field) in children { + out.push('\n'); + out.push_str(&format_node_with_field( + child, + child_field, + source, + depth + 1, + include_anonymous, + )); + } + out.push(')'); + out +} + +fn escape_string(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '\n' => result.push_str("\\n"), + '\r' => result.push_str("\\r"), + '\t' => result.push_str("\\t"), + '\\' => result.push_str("\\\\"), + '"' => result.push_str("\\\""), + c if c.is_control() => result.push_str(&format!("\\u{{{:04x}}}", c as u32)), + c => result.push(c), + } + } + result +} diff --git a/crates/plotnik-cli/src/commands/mod.rs b/crates/plotnik-cli/src/commands/mod.rs index 9a93451b..f0644b3d 100644 --- a/crates/plotnik-cli/src/commands/mod.rs +++ b/crates/plotnik-cli/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod ast; pub mod check; pub mod dump; pub mod exec; @@ -7,7 +8,6 @@ pub mod langs; pub mod query_loader; pub mod run_common; pub mod trace; -pub mod tree; #[cfg(test)] mod langs_tests; diff --git a/crates/plotnik-cli/src/commands/tree.rs b/crates/plotnik-cli/src/commands/tree.rs deleted file mode 100644 index 55cd4ab5..00000000 --- a/crates/plotnik-cli/src/commands/tree.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::fs; -use std::io::{self, Read}; -use std::path::{Path, PathBuf}; - -use arborium_tree_sitter as tree_sitter; -use plotnik_langs::Lang; - -pub struct TreeArgs { - pub source_path: Option, - pub source_text: Option, - pub lang: Option, - pub raw: bool, - pub spans: bool, -} - -pub fn run(args: TreeArgs) { - let source = match (&args.source_text, &args.source_path) { - (Some(text), None) => text.clone(), - (None, Some(path)) => load_source(path), - (Some(_), Some(_)) => { - eprintln!("error: cannot use both --source and positional SOURCE"); - std::process::exit(1); - } - (None, None) => { - eprintln!("error: source required (positional or --source)"); - std::process::exit(1); - } - }; - - let lang = resolve_lang( - &args.lang, - args.source_path.as_deref(), - args.source_text.is_some(), - ); - let tree = lang.parse(&source); - print!("{}", dump_tree(&tree, &source, args.raw, args.spans)); -} - -fn load_source(path: &PathBuf) -> String { - if path.as_os_str() == "-" { - let mut buf = String::new(); - io::stdin() - .read_to_string(&mut buf) - .expect("failed to read stdin"); - return buf; - } - fs::read_to_string(path).unwrap_or_else(|_| { - eprintln!("error: file not found: {}", path.display()); - std::process::exit(1); - }) -} - -fn resolve_lang(lang: &Option, source_path: Option<&Path>, is_inline: bool) -> Lang { - if let Some(name) = lang { - return plotnik_langs::from_name(name).unwrap_or_else(|| { - eprintln!("error: unknown language: {}", name); - std::process::exit(1); - }); - } - - if let Some(path) = source_path - && path.as_os_str() != "-" - && let Some(ext) = path.extension().and_then(|e| e.to_str()) - { - return plotnik_langs::from_ext(ext).unwrap_or_else(|| { - eprintln!( - "error: cannot infer language from extension '.{}', use -l/--lang", - ext - ); - std::process::exit(1); - }); - } - - if is_inline { - eprintln!("error: -l/--lang is required when using inline source"); - } else { - eprintln!("error: -l/--lang is required (cannot infer from stdin)"); - } - std::process::exit(1); -} - -fn dump_tree(tree: &tree_sitter::Tree, source: &str, raw: bool, spans: bool) -> String { - format_node(tree.root_node(), source, 0, raw, spans) + "\n" -} - -fn format_node( - node: tree_sitter::Node, - source: &str, - depth: usize, - include_anonymous: bool, - show_spans: bool, -) -> String { - format_node_with_field(node, None, source, depth, include_anonymous, show_spans) -} - -fn format_node_with_field( - node: tree_sitter::Node, - field_name: Option<&str>, - source: &str, - depth: usize, - include_anonymous: bool, - show_spans: bool, -) -> String { - if !include_anonymous && !node.is_named() { - return String::new(); - } - - let indent = " ".repeat(depth); - let kind = node.kind(); - let field_prefix = field_name.map(|f| format!("{}: ", f)).unwrap_or_default(); - let span_suffix = if show_spans { - let start = node.start_position(); - let end = node.end_position(); - format!( - " [{}:{}-{}:{}]", - start.row, start.column, end.row, end.column - ) - } else { - String::new() - }; - - let children: Vec<_> = { - let mut cursor = node.walk(); - let mut result = Vec::new(); - if cursor.goto_first_child() { - loop { - let child = cursor.node(); - if include_anonymous || child.is_named() { - result.push((child, cursor.field_name())); - } - if !cursor.goto_next_sibling() { - break; - } - } - } - result - }; - - if children.is_empty() { - let text = node - .utf8_text(source.as_bytes()) - .unwrap_or(""); - return if text == kind { - format!( - "{}{}(\"{}\"){}", - indent, - field_prefix, - escape_string(kind), - span_suffix - ) - } else { - format!( - "{}{}({} \"{}\"){}", - indent, - field_prefix, - kind, - escape_string(text), - span_suffix - ) - }; - } - - let mut out = format!("{}{}({}{}", indent, field_prefix, kind, span_suffix); - for (child, child_field) in children { - out.push('\n'); - out.push_str(&format_node_with_field( - child, - child_field, - source, - depth + 1, - include_anonymous, - show_spans, - )); - } - out.push(')'); - out -} - -fn escape_string(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - for c in s.chars() { - match c { - '\n' => result.push_str("\\n"), - '\r' => result.push_str("\\r"), - '\t' => result.push_str("\\t"), - '\\' => result.push_str("\\\\"), - '"' => result.push_str("\\\""), - c if c.is_control() => result.push_str(&format!("\\u{{{:04x}}}", c as u32)), - c => result.push(c), - } - } - result -} diff --git a/crates/plotnik-cli/src/main.rs b/crates/plotnik-cli/src/main.rs index acec48af..54c8b7e1 100644 --- a/crates/plotnik-cli/src/main.rs +++ b/crates/plotnik-cli/src/main.rs @@ -2,7 +2,7 @@ mod cli; mod commands; use cli::{ - CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams, TreeParams, + AstParams, CheckParams, DumpParams, ExecParams, InferParams, LangsParams, TraceParams, build_cli, }; @@ -10,9 +10,9 @@ fn main() { let matches = build_cli().get_matches(); match matches.subcommand() { - Some(("tree", m)) => { - let params = TreeParams::from_matches(m); - commands::tree::run(params.into()); + Some(("ast", m)) => { + let params = AstParams::from_matches(m); + commands::ast::run(params.into()); } Some(("check", m)) => { let params = CheckParams::from_matches(m);