Skip to content

Commit 94bce15

Browse files
committed
v2: add cli
1 parent 44b705a commit 94bce15

File tree

3 files changed

+217
-0
lines changed

3 files changed

+217
-0
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ wasm-bindgen-test = "0.3.34"
4242
web-sys = "0.3.77"
4343
console_error_panic_hook = "0.1.7"
4444
console_log = "1.0.0"
45+
annotate-snippets = "0.11.5"
46+
anyhow = "1.0.94"
4547

4648
# local
4749
squawk-parser = { version = "0.0.0", path = "./crates/parser" }

crates/squawk_cli/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "squawk_cli"
3+
version = "0.0.0"
4+
description = "TBD"
5+
default-run = "squawk_cli"
6+
7+
authors.workspace = true
8+
edition.workspace = true
9+
license.workspace = true
10+
rust-version.workspace = true
11+
12+
[dependencies]
13+
clap.workspace = true
14+
anyhow.workspace = true
15+
enum-iterator.workspace = true
16+
annotate-snippets.workspace = true
17+
atty.workspace = true
18+
19+
squawk_syntax.workspace = true
20+
squawk_lexer.workspace = true
21+
squawk_linter.workspace = true
22+
23+
[lints]
24+
workspace = true

crates/squawk_cli/src/main.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use std::{
2+
io::{self, Read},
3+
process::ExitCode,
4+
};
5+
6+
use annotate_snippets::{Level, Renderer, Snippet};
7+
use anyhow::Result;
8+
use atty::Stream;
9+
use clap::{error::ErrorKind, CommandFactory, Parser, ValueEnum};
10+
use squawk_linter::Violation;
11+
use squawk_syntax::syntax_error::SyntaxError;
12+
use std::{fs, path::PathBuf};
13+
14+
#[derive(ValueEnum, Clone, Debug)]
15+
enum Mode {
16+
Parse,
17+
Lex,
18+
Lint,
19+
}
20+
21+
/// Dump Parse/Lex Data
22+
#[derive(Parser, Debug)]
23+
#[command(version, about, long_about = None)]
24+
struct Args {
25+
/// SQL to dump
26+
#[arg(short, long, conflicts_with = "file")]
27+
sql: Option<String>,
28+
29+
/// Path to read SQL
30+
#[arg(short, long, conflicts_with = "sql")]
31+
file: Option<PathBuf>,
32+
33+
/// Either Parser debug output or Lexer debug output.
34+
#[arg(short, long, default_value = "parse")]
35+
mode: Mode,
36+
37+
#[arg(short, long, default_value_t = false)]
38+
verbose: bool,
39+
40+
/// Assume the SQL is being run within a transaction. No explicit begin,
41+
/// commit required.
42+
#[arg(short, long)]
43+
assume_in_transaction: bool,
44+
}
45+
46+
fn read_stdin() -> Result<String> {
47+
let mut buffer = String::new();
48+
let stdin = io::stdin();
49+
let mut handle = stdin.lock();
50+
handle.read_to_string(&mut buffer)?;
51+
Ok(buffer)
52+
}
53+
54+
fn read_sql(arg_sql: Option<String>, file: &Option<PathBuf>) -> Result<String> {
55+
let is_stdin = !atty::is(Stream::Stdin);
56+
if is_stdin {
57+
read_stdin()
58+
} else if let Some(path) = &file {
59+
Ok(fs::read_to_string(path)?)
60+
} else if let Some(sql) = arg_sql {
61+
Ok(sql)
62+
} else {
63+
let err = Args::command().error(
64+
ErrorKind::ArgumentConflict,
65+
"--sql, --file, or stdin must be provided.",
66+
);
67+
err.exit()
68+
}
69+
}
70+
71+
fn main() -> Result<ExitCode> {
72+
let args = Args::parse();
73+
let sql = read_sql(args.sql, &args.file)?;
74+
let filename = args
75+
.file
76+
.map(|x| x.display().to_string())
77+
.unwrap_or("stdin".to_string());
78+
match args.mode {
79+
Mode::Lex => {
80+
let tokens = squawk_lexer::tokenize(&sql);
81+
let mut start = 0;
82+
for token in tokens {
83+
if args.verbose {
84+
let content = &sql[start as usize..(start + token.len) as usize];
85+
start += token.len;
86+
println!("{:?} @ {:?}", content, token.kind);
87+
} else {
88+
println!("{:?}", token);
89+
}
90+
}
91+
Ok(ExitCode::SUCCESS)
92+
}
93+
Mode::Parse => {
94+
let parse = squawk_syntax::SourceFile::parse(&sql);
95+
if args.verbose {
96+
println!("{}\n---", parse.syntax_node());
97+
}
98+
print!("{:#?}", parse.syntax_node());
99+
let errors = parse.errors();
100+
if !errors.is_empty() {
101+
let mut snap = "---".to_string();
102+
for syntax_error in &errors {
103+
let range = syntax_error.range();
104+
let text = syntax_error.message();
105+
// split into there own lines so that we can just grep
106+
// for error without hitting this part
107+
snap += "\n";
108+
snap += "ERROR";
109+
if range.start() == range.end() {
110+
snap += &format!("@{:?} {:?}", range.start(), text);
111+
} else {
112+
snap += &format!("@{:?}:{:?} {:?}", range.start(), range.end(), text);
113+
}
114+
}
115+
println!("{}", snap);
116+
117+
render_syntax_errors(&errors, &filename, &sql);
118+
119+
return Ok(ExitCode::FAILURE);
120+
}
121+
Ok(ExitCode::SUCCESS)
122+
}
123+
Mode::Lint => {
124+
let mut linter = squawk_linter::squawk_linter::with_all_rules();
125+
linter.settings.assume_in_transaction = args.assume_in_transaction;
126+
let parse = squawk_syntax::SourceFile::parse(&sql);
127+
128+
if args.verbose {
129+
println!("{}\n---", parse.syntax_node());
130+
// print!("{:#?}\n---", parse.syntax_node());
131+
}
132+
133+
let errors = linter.lint(parse, &sql);
134+
135+
if errors.is_empty() {
136+
Ok(ExitCode::SUCCESS)
137+
} else {
138+
render_lint_errors(&errors, &filename, &sql);
139+
println!();
140+
println!("Find detailed examples and solutions for each rule at https://squawkhq.com/docs/rules");
141+
println!(
142+
"Found {} issue in 1 file (checked 1 source file)",
143+
errors.len()
144+
);
145+
Ok(ExitCode::FAILURE)
146+
}
147+
}
148+
}
149+
}
150+
151+
fn render_syntax_errors(errors: &[SyntaxError], filename: &str, sql: &str) {
152+
let renderer = Renderer::styled();
153+
for err in errors {
154+
let text = err.message();
155+
let span = err.range().into();
156+
let message = Level::Warning.title(text).id("syntax-error").snippet(
157+
Snippet::source(sql)
158+
.origin(filename)
159+
.fold(true)
160+
.annotation(Level::Error.span(span)),
161+
);
162+
println!("{}", renderer.render(message));
163+
}
164+
}
165+
166+
fn render_lint_errors(errors: &Vec<&Violation>, filename: &str, sql: &str) {
167+
let renderer = Renderer::styled();
168+
for err in errors {
169+
let meta = err.code.meta();
170+
let footers = err.messages.iter().map(|e| Level::Help.title(e));
171+
// TODO: we need to figure out error messages, they shouldn't be in two places
172+
let prebuilt_footers = meta.messages.into_iter().map(|x| match x {
173+
squawk_linter::ViolationMessage::Note(x) => Level::Note.title(x),
174+
squawk_linter::ViolationMessage::Help(x) => Level::Help.title(x),
175+
});
176+
let error_name = err.code.to_string();
177+
let message = Level::Warning
178+
.title(&meta.title)
179+
.id(&error_name)
180+
.snippet(
181+
Snippet::source(sql)
182+
.origin(filename)
183+
.fold(true)
184+
.annotation(Level::Error.span(err.text_range.into())),
185+
)
186+
.footers(footers)
187+
.footers(prebuilt_footers);
188+
189+
println!("{}", renderer.render(message));
190+
}
191+
}

0 commit comments

Comments
 (0)