Skip to content

Commit a721772

Browse files
authored
Visualise syntax tree from CLI (#324)
* Exposed --visualise CLI option * Added serde and serde_json dependencies for the CLI * Decant Tree-sitter's parsed tree into a recursive structure. * Use Serde to serialise that structure to JSON.
1 parent 49c5dd6 commit a721772

File tree

12 files changed

+256
-73
lines changed

12 files changed

+256
-73
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ itertools = "0.10"
1313
log = "0.4"
1414
ouroboros = "0.15"
1515
pretty_assertions = "1.3"
16+
serde = { version = "1.0", features = ["derive"] }
17+
serde_json = "1.0"
1618
tempfile = "3.4.0"
1719
test-log = "0.2"
1820
tree-sitter = "0.20"

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ Options:
163163
Format the input file in place. (This has the effect of setting the
164164
output file equal to the input file.)
165165

166+
* `-v`, `--visualise`\
167+
Visualise the syntax tree, rather than format [possible values: `json`
168+
(default)].
169+
166170
* `-s`, `--skip-idempotence`\
167171
Do not check that formatting twice gives the same output.
168172

benches/benchmark.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use criterion::{criterion_group, criterion_main, Criterion};
22
use std::fs;
33
use std::io;
4-
use topiary::formatter;
4+
use topiary::{formatter, Operation};
55

66
fn criterion_benchmark(c: &mut Criterion) {
77
let input = fs::read_to_string("tests/samples/input/ocaml.ml").unwrap();
@@ -13,7 +13,15 @@ fn criterion_benchmark(c: &mut Criterion) {
1313
let mut query = query.as_bytes();
1414
let mut output = io::BufWriter::new(Vec::new());
1515

16-
formatter(&mut input, &mut output, &mut query, None, false)
16+
formatter(
17+
&mut input,
18+
&mut output,
19+
&mut query,
20+
None,
21+
Operation::Format {
22+
skip_idempotence: false,
23+
},
24+
)
1725
})
1826
});
1927
}

src/bin/topiary/error.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub type CLIResult<T> = result::Result<T, TopiaryError>;
1010
#[derive(Debug)]
1111
pub enum TopiaryError {
1212
Lib(FormatterError),
13-
Bin(String, CLIError),
13+
Bin(String, Option<CLIError>),
1414
}
1515

1616
/// A subtype of `TopiaryError::Bin`
@@ -33,8 +33,9 @@ impl error::Error for TopiaryError {
3333
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
3434
match self {
3535
Self::Lib(error) => error.source(),
36-
Self::Bin(_, CLIError::IOError(error)) => error.source(),
37-
Self::Bin(_, CLIError::Generic(error)) => error.source(),
36+
Self::Bin(_, Some(CLIError::IOError(error))) => error.source(),
37+
Self::Bin(_, Some(CLIError::Generic(error))) => error.source(),
38+
Self::Bin(_, None) => None,
3839
}
3940
}
4041
}
@@ -48,11 +49,13 @@ impl From<FormatterError> for TopiaryError {
4849
impl From<io::Error> for TopiaryError {
4950
fn from(e: io::Error) -> Self {
5051
match e.kind() {
51-
io::ErrorKind::NotFound => Self::Bin("File not found".into(), CLIError::IOError(e)),
52+
io::ErrorKind::NotFound => {
53+
Self::Bin("File not found".into(), Some(CLIError::IOError(e)))
54+
}
5255

5356
_ => Self::Bin(
54-
"Cound not read or write to file".into(),
55-
CLIError::IOError(e),
57+
"Could not read or write to file".into(),
58+
Some(CLIError::IOError(e)),
5659
),
5760
}
5861
}
@@ -62,7 +65,7 @@ impl From<tempfile::PersistError> for TopiaryError {
6265
fn from(e: tempfile::PersistError) -> Self {
6366
Self::Bin(
6467
"Could not persist output to disk".into(),
65-
CLIError::IOError(e.error),
68+
Some(CLIError::IOError(e.error)),
6669
)
6770
}
6871
}
@@ -75,8 +78,8 @@ where
7578
{
7679
fn from(e: io::IntoInnerError<W>) -> Self {
7780
Self::Bin(
78-
"Cannot flush internal buffer".into(),
79-
CLIError::Generic(Box::new(e)),
81+
"Could not flush internal buffer".into(),
82+
Some(CLIError::Generic(Box::new(e))),
8083
)
8184
}
8285
}

src/bin/topiary/main.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
mod error;
22
mod output;
33
mod supported;
4+
mod visualise;
45

5-
use crate::{error::CLIResult, output::OutputFile, supported::SupportedLanguage};
6-
use clap::{ArgGroup, Parser};
76
use std::{
87
error::Error,
98
fs::File,
109
io::{stdin, BufReader, BufWriter},
1110
path::PathBuf,
1211
};
13-
use topiary::{formatter, Language};
12+
13+
use clap::{ArgGroup, Parser};
14+
15+
use crate::{
16+
error::CLIResult, output::OutputFile, supported::SupportedLanguage, visualise::Visualisation,
17+
};
18+
use topiary::{formatter, Language, Operation};
1419

1520
#[derive(Parser, Debug)]
1621
#[clap(author, version, about, long_about = None)]
1722
// Require at least one of --language, --input-file or --query (n.b., language > input > query)
1823
#[clap(group(ArgGroup::new("rule").multiple(true).required(true).args(&["language", "input-file", "query"]),))]
1924
struct Args {
2025
/// Which language to parse and format
21-
#[clap(short, long, arg_enum, display_order = 1)]
26+
#[clap(short, long, value_enum, display_order = 1)]
2227
language: Option<SupportedLanguage>,
2328

2429
/// Path to an input file. If omitted, or equal to "-", read from standard
@@ -39,8 +44,21 @@ struct Args {
3944
#[clap(short, long, requires = "input-file", display_order = 5)]
4045
in_place: bool,
4146

47+
/// Visualise the syntax tree, rather than format.
48+
#[clap(
49+
short,
50+
long,
51+
value_enum,
52+
aliases = &["view", "visualize"],
53+
value_name = "OUTPUT_FORMAT",
54+
conflicts_with_all = &["in-place", "skip-idempotence"],
55+
default_missing_value = "json",
56+
display_order = 6
57+
)]
58+
visualise: Option<Visualisation>,
59+
4260
/// Do not check that formatting twice gives the same output
43-
#[clap(short, long, display_order = 6)]
61+
#[clap(short, long, display_order = 7)]
4462
skip_idempotence: bool,
4563
}
4664

@@ -94,13 +112,17 @@ fn run() -> CLIResult<()> {
94112

95113
let mut query = BufReader::new(File::open(query_path)?);
96114

97-
formatter(
98-
&mut input,
99-
&mut output,
100-
&mut query,
101-
language,
102-
args.skip_idempotence,
103-
)?;
115+
let operation = if let Some(visualisation) = args.visualise {
116+
Operation::Visualise {
117+
output_format: visualisation.into(),
118+
}
119+
} else {
120+
Operation::Format {
121+
skip_idempotence: args.skip_idempotence,
122+
}
123+
};
124+
125+
formatter(&mut input, &mut output, &mut query, language, operation)?;
104126

105127
output.into_inner()?.persist()?;
106128

src/bin/topiary/supported.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use clap::ArgEnum;
1+
use clap::ValueEnum;
22
use topiary::Language;
33

4-
#[derive(ArgEnum, Clone, Copy, Debug)]
4+
#[derive(ValueEnum, Clone, Copy, Debug)]
55
pub enum SupportedLanguage {
66
Json,
77
Toml,

src/bin/topiary/visualise.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// This is somewhat redundant, but we cannot implement clap::ValueEnum for topiary::Visualisation
2+
// without breaking the orphan rules. So we have to maintain a local copy for the sake of the CLI.
3+
4+
use clap::ValueEnum;
5+
6+
#[derive(ValueEnum, Clone, Copy, Debug)]
7+
pub enum Visualisation {
8+
Json,
9+
}
10+
11+
impl From<Visualisation> for topiary::Visualisation {
12+
fn from(visualisation: Visualisation) -> Self {
13+
match visualisation {
14+
Visualisation::Json => Self::Json,
15+
}
16+
}
17+
}

src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,9 @@ where
169169
))
170170
}
171171
}
172+
173+
impl From<serde_json::Error> for FormatterError {
174+
fn from(e: serde_json::Error) -> Self {
175+
Self::Internal("Could not serialise JSON output".into(), Some(e.into()))
176+
}
177+
}

0 commit comments

Comments
 (0)