Skip to content

Commit e0d5dd2

Browse files
committed
Add dependencies for plotnik-cli and implement CLI structure
1 parent eb2d2ae commit e0d5dd2

File tree

23 files changed

+608
-212
lines changed

23 files changed

+608
-212
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/plotnik-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ edition = "2024"
55

66
[dependencies]
77
clap = { version = "4.5.51", features = ["derive"] }
8+
plotnik-lib = { path = "../plotnik-lib" }
9+
rowan = "0.16"

crates/plotnik-cli/src/main.rs

Lines changed: 256 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,258 @@
1+
use clap::{Args, Parser, Subcommand};
2+
use plotnik_lib::Query;
3+
use std::fs;
4+
use std::io::{self, Read};
5+
use std::path::PathBuf;
6+
7+
#[derive(Parser)]
8+
#[command(name = "plotnik", bin_name = "plotnik")]
9+
#[command(about = "Query language for tree-sitter AST with type inference")]
10+
#[command(after_help = r#"OUTPUT DEPENDENCIES:
11+
┌─────────────────┬─────────────┬──────────────┐
12+
│ Output │ Needs Query │ Needs Source │
13+
├─────────────────┼─────────────┼──────────────┤
14+
│ --query-cst │ ✓ │ │
15+
│ --query-refs │ ✓ │ │
16+
│ --query-types │ ✓ │ │
17+
│ --source-ast │ │ ✓ │
18+
│ --trace │ ✓ │ ✓ │
19+
│ --result │ ✓ │ ✓ │
20+
└─────────────────┴─────────────┴──────────────┘
21+
22+
EXAMPLES:
23+
# Parse and typecheck query only
24+
plotnik --query-text '(identifier) @id' --query-cst --query-types
25+
26+
# Dump tree-sitter AST of source file
27+
plotnik --source-file app.ts --source-ast
28+
29+
# Full pipeline: match query against source
30+
plotnik --query-file rules.pql --source-file app.ts --result
31+
32+
# Debug with trace
33+
plotnik --query-text '(function_declaration) @fn' \
34+
--source-text 'function foo() {}' --lang typescript --trace
35+
36+
# Show documentation
37+
plotnik docs reference"#)]
38+
struct Cli {
39+
#[command(subcommand)]
40+
command: Option<Command>,
41+
42+
#[command(flatten)]
43+
query: QueryArgs,
44+
45+
#[command(flatten)]
46+
source: SourceArgs,
47+
48+
/// Language for source (required for --source-text, inferred from extension for --source-file)
49+
#[arg(long, short = 'l', value_name = "LANG")]
50+
lang: Option<String>,
51+
52+
#[command(flatten)]
53+
output: OutputArgs,
54+
}
55+
56+
#[derive(Subcommand)]
57+
enum Command {
58+
/// Print documentation
59+
Docs {
60+
/// Topic to display (e.g., "reference", "examples")
61+
topic: Option<String>,
62+
},
63+
}
64+
65+
#[derive(Args)]
66+
#[group(id = "query_input", multiple = false)]
67+
struct QueryArgs {
68+
/// Query as inline text
69+
#[arg(long, value_name = "QUERY")]
70+
query_text: Option<String>,
71+
72+
/// Query from file (use "-" for stdin)
73+
#[arg(long, value_name = "FILE")]
74+
query_file: Option<PathBuf>,
75+
}
76+
77+
#[derive(Args)]
78+
#[group(id = "source_input", multiple = false)]
79+
struct SourceArgs {
80+
/// Source code as inline text
81+
#[arg(long, value_name = "SOURCE")]
82+
source_text: Option<String>,
83+
84+
/// Source code from file (use "-" for stdin)
85+
#[arg(long, value_name = "FILE")]
86+
source_file: Option<PathBuf>,
87+
}
88+
89+
#[derive(Args)]
90+
struct OutputArgs {
91+
/// Show parsed query CST (concrete syntax tree)
92+
#[arg(long)]
93+
query_cst: bool,
94+
95+
/// Show name resolution (definitions and references)
96+
#[arg(long)]
97+
query_refs: bool,
98+
99+
/// Show inferred output types
100+
#[arg(long)]
101+
query_types: bool,
102+
103+
/// Show tree-sitter AST of source
104+
#[arg(long)]
105+
source_ast: bool,
106+
107+
/// Show execution trace
108+
#[arg(long)]
109+
trace: bool,
110+
111+
/// Show match results
112+
#[arg(long)]
113+
result: bool,
114+
}
115+
1116
fn main() {
2-
println!("Hello, world!");
117+
let cli = Cli::parse();
118+
119+
if let Some(Command::Docs { topic }) = cli.command {
120+
print_help(topic.as_deref());
121+
return;
122+
}
123+
124+
let has_query = cli.query.query_text.is_some() || cli.query.query_file.is_some();
125+
let has_source = cli.source.source_text.is_some() || cli.source.source_file.is_some();
126+
127+
// Validate output dependencies
128+
if (cli.output.query_cst || cli.output.query_refs || cli.output.query_types) && !has_query {
129+
eprintln!("error: --query-cst, --query-refs, and --query-types require --query-text or --query-file");
130+
std::process::exit(1);
131+
}
132+
133+
if cli.output.source_ast && !has_source {
134+
eprintln!("error: --source-ast requires --source-text or --source-file");
135+
std::process::exit(1);
136+
}
137+
138+
if cli.output.trace && !(has_query && has_source) {
139+
eprintln!("error: --trace requires both query and source inputs");
140+
std::process::exit(1);
141+
}
142+
143+
if cli.output.result && !(has_query && has_source) {
144+
eprintln!("error: --result requires both query and source inputs");
145+
std::process::exit(1);
146+
}
147+
148+
// If both inputs provided and no outputs selected, default to --result
149+
let show_result = cli.output.result
150+
|| (has_query
151+
&& has_source
152+
&& !cli.output.query_cst
153+
&& !cli.output.query_refs
154+
&& !cli.output.query_types
155+
&& !cli.output.source_ast
156+
&& !cli.output.trace);
157+
158+
// Validate --lang requirement
159+
if cli.source.source_text.is_some() && cli.lang.is_none() {
160+
eprintln!("error: --lang is required when using --source-text");
161+
std::process::exit(1);
162+
}
163+
164+
// Load query if needed
165+
let query_source = if has_query {
166+
Some(load_query(&cli.query))
167+
} else {
168+
None
169+
};
170+
171+
let query = query_source.as_ref().map(|src| Query::new(src));
172+
173+
if cli.output.query_cst {
174+
println!("=== QUERY CST ===");
175+
if let Some(ref q) = query {
176+
print!("{}", q.format_cst());
177+
}
178+
}
179+
180+
if cli.output.query_refs {
181+
println!("=== QUERY REFS ===");
182+
if let Some(ref q) = query {
183+
print!("{}", q.format_refs());
184+
}
185+
}
186+
187+
if cli.output.query_types {
188+
println!("=== QUERY TYPES ===");
189+
println!("(not implemented)");
190+
println!();
191+
}
192+
193+
if cli.output.source_ast {
194+
println!("=== SOURCE AST ===");
195+
println!("(not implemented)");
196+
println!();
197+
}
198+
199+
if cli.output.trace {
200+
println!("=== TRACE ===");
201+
println!("(not implemented)");
202+
println!();
203+
}
204+
205+
if show_result {
206+
println!("=== RESULT ===");
207+
println!("(not implemented)");
208+
println!();
209+
}
210+
211+
// Print query errors at the end, grouped by stage
212+
if let Some(ref q) = query {
213+
if !q.is_valid() {
214+
eprint!("{}", q.render_errors_grouped());
215+
}
216+
}
217+
}
218+
219+
fn load_query(args: &QueryArgs) -> String {
220+
if let Some(ref text) = args.query_text {
221+
return text.clone();
222+
}
223+
if let Some(ref path) = args.query_file {
224+
if path.as_os_str() == "-" {
225+
let mut buf = String::new();
226+
io::stdin().read_to_string(&mut buf).expect("failed to read stdin");
227+
return buf;
228+
}
229+
return fs::read_to_string(path).expect("failed to read query file");
230+
}
231+
unreachable!()
3232
}
233+
234+
235+
236+
fn print_help(topic: Option<&str>) {
237+
match topic {
238+
None => {
239+
println!("Available topics:");
240+
println!(" reference - Query language reference");
241+
println!(" examples - Example queries");
242+
println!();
243+
println!("Usage: plotnik docs <topic>");
244+
}
245+
Some("reference") => {
246+
// TODO: include_str! the actual REFERENCE.md
247+
println!("{}", include_str!("../../../docs/REFERENCE.md"));
248+
}
249+
Some("examples") => {
250+
println!("(examples not yet written)");
251+
}
252+
Some(other) => {
253+
eprintln!("Unknown help topic: {}", other);
254+
eprintln!("Run 'plotnik help' to see available topics");
255+
std::process::exit(1);
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)