From 97fd990822deeb3c288f4999a8410fba69f230b6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Feb 2023 12:33:34 -0800 Subject: [PATCH] Add --dot flag to parse subcommand, for printing tree as DOT graph --- cli/src/main.rs | 20 ++++++--- cli/src/parse.rs | 19 +++++++-- cli/src/util.rs | 80 ++++++++++++++++++++++------------- lib/binding_rust/bindings.rs | 7 +++ lib/binding_rust/lib.rs | 10 +++++ lib/include/tree_sitter/api.h | 2 +- lib/src/clock.h | 1 + lib/src/tree.c | 5 ++- lib/src/tree.h | 2 + script/generate-bindings | 3 -- 10 files changed, 106 insertions(+), 43 deletions(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index e0625708..47e7597b 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -3,6 +3,7 @@ use clap::{App, AppSettings, Arg, SubCommand}; use glob::glob; use std::path::{Path, PathBuf}; use std::{env, fs, u64}; +use tree_sitter_cli::parse::ParseOutput; use tree_sitter_cli::{ generate, highlight, logger, parse, playground, query, tags, test, test_highlight, test_tags, util, wasm, @@ -136,7 +137,8 @@ fn run() -> Result<()> { .arg(&debug_arg) .arg(&debug_build_arg) .arg(&debug_graph_arg) - .arg(Arg::with_name("debug-xml").long("xml").short("x")) + .arg(Arg::with_name("output-dot").long("dot")) + .arg(Arg::with_name("output-xml").long("xml").short("x")) .arg( Arg::with_name("stat") .help("Show parsing statistic") @@ -377,8 +379,17 @@ fn run() -> Result<()> { let debug = matches.is_present("debug"); let debug_graph = matches.is_present("debug-graph"); let debug_build = matches.is_present("debug-build"); - let debug_xml = matches.is_present("debug-xml"); - let quiet = matches.is_present("quiet"); + + let output = if matches.is_present("output-dot") { + ParseOutput::Dot + } else if matches.is_present("output-xml") { + ParseOutput::Xml + } else if matches.is_present("quiet") { + ParseOutput::Quiet + } else { + ParseOutput::Normal + }; + let time = matches.is_present("time"); let edits = matches .values_of("edits") @@ -416,12 +427,11 @@ fn run() -> Result<()> { path, &edits, max_path_length, - quiet, + output, time, timeout, debug, debug_graph, - debug_xml, Some(&cancellation_flag), )?; diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 15a9d4c9..3e28e51a 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -30,17 +30,24 @@ impl fmt::Display for Stats { } } +#[derive(Copy, Clone)] +pub enum ParseOutput { + Normal, + Quiet, + Xml, + Dot, +} + pub fn parse_file_at_path( language: Language, path: &Path, edits: &Vec<&str>, max_path_length: usize, - quiet: bool, + output: ParseOutput, print_time: bool, timeout: u64, debug: bool, debug_graph: bool, - debug_xml: bool, cancellation_flag: Option<&AtomicUsize>, ) -> Result { let mut _log_session = None; @@ -95,7 +102,7 @@ pub fn parse_file_at_path( let duration_ms = duration.as_secs() * 1000 + duration.subsec_nanos() as u64 / 1000000; let mut cursor = tree.walk(); - if !quiet { + if matches!(output, ParseOutput::Normal) { let mut needs_newline = false; let mut indent_level = 0; let mut did_visit_children = false; @@ -151,7 +158,7 @@ pub fn parse_file_at_path( println!(""); } - if debug_xml { + if matches!(output, ParseOutput::Xml) { let mut needs_newline = false; let mut indent_level = 0; let mut did_visit_children = false; @@ -206,6 +213,10 @@ pub fn parse_file_at_path( println!(""); } + if matches!(output, ParseOutput::Dot) { + util::print_tree_graph(&tree, "log.html").unwrap(); + } + let mut first_error = None; loop { let node = cursor.node(); diff --git a/cli/src/util.rs b/cli/src/util.rs index acd8acbf..2b7405fd 100644 --- a/cli/src/util.rs +++ b/cli/src/util.rs @@ -3,7 +3,7 @@ use std::io; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; -use tree_sitter::Parser; +use tree_sitter::{Parser, Tree}; #[cfg(unix)] use anyhow::{anyhow, Context}; @@ -29,39 +29,61 @@ pub fn cancel_on_stdin() -> Arc { } result } + #[cfg(windows)] -pub struct LogSession(); +pub struct LogSession; #[cfg(unix)] -pub struct LogSession(PathBuf, Option, Option); +pub struct LogSession { + path: PathBuf, + dot_process: Option, + dot_process_stdin: Option, +} + +pub fn print_tree_graph(tree: &Tree, path: &str) -> Result<()> { + let session = LogSession::new(path)?; + tree.print_dot_graph(session.dot_process_stdin.as_ref().unwrap()); + Ok(()) +} + +pub fn log_graphs(parser: &mut Parser, path: &str) -> Result { + let session = LogSession::new(path)?; + parser.print_dot_graphs(session.dot_process_stdin.as_ref().unwrap()); + Ok(session) +} #[cfg(windows)] -pub fn log_graphs(_parser: &mut Parser, _path: &str) -> Result { - Ok(LogSession()) +impl LogSession { + fn new(path: &str) -> Result { + Ok(Self) + } } #[cfg(unix)] -pub fn log_graphs(parser: &mut Parser, path: &str) -> Result { - use std::io::Write; +impl LogSession { + fn new(path: &str) -> Result { + use std::io::Write; - let mut dot_file = std::fs::File::create(path)?; - dot_file.write(HTML_HEADER)?; - let mut dot_process = Command::new("dot") - .arg("-Tsvg") - .stdin(Stdio::piped()) - .stdout(dot_file) - .spawn() - .with_context(|| "Failed to run the `dot` command. Check that graphviz is installed.")?; - let dot_stdin = dot_process - .stdin - .take() - .ok_or_else(|| anyhow!("Failed to open stdin for `dot` process."))?; - parser.print_dot_graphs(&dot_stdin); - Ok(LogSession( - PathBuf::from(path), - Some(dot_process), - Some(dot_stdin), - )) + let mut dot_file = std::fs::File::create(path)?; + dot_file.write(HTML_HEADER)?; + let mut dot_process = Command::new("dot") + .arg("-Tsvg") + .stdin(Stdio::piped()) + .stdout(dot_file) + .spawn() + .with_context(|| { + "Failed to run the `dot` command. Check that graphviz is installed." + })?; + let dot_stdin = dot_process + .stdin + .take() + .ok_or_else(|| anyhow!("Failed to open stdin for `dot` process."))?; + Ok(Self { + path: PathBuf::from(path), + dot_process: Some(dot_process), + dot_process_stdin: Some(dot_stdin), + }) + } } #[cfg(unix)] @@ -69,13 +91,13 @@ impl Drop for LogSession { fn drop(&mut self) { use std::fs; - drop(self.2.take().unwrap()); - let output = self.1.take().unwrap().wait_with_output().unwrap(); + drop(self.dot_process_stdin.take().unwrap()); + let output = self.dot_process.take().unwrap().wait_with_output().unwrap(); if output.status.success() { if cfg!(target_os = "macos") - && fs::metadata(&self.0).unwrap().len() > HTML_HEADER.len() as u64 + && fs::metadata(&self.path).unwrap().len() > HTML_HEADER.len() as u64 { - Command::new("open").arg(&self.0).output().unwrap(); + Command::new("open").arg(&self.path).output().unwrap(); } } else { eprintln!( diff --git a/lib/binding_rust/bindings.rs b/lib/binding_rust/bindings.rs index b4ec9bed..4591a380 100644 --- a/lib/binding_rust/bindings.rs +++ b/lib/binding_rust/bindings.rs @@ -346,6 +346,9 @@ extern "C" { pub fn ts_tree_language(arg1: *const TSTree) -> *const TSLanguage; } extern "C" { + #[doc = " Get the array of included ranges that was used to parse the syntax tree."] + #[doc = ""] + #[doc = " The returned pointer must be freed by the caller."] pub fn ts_tree_included_ranges(arg1: *const TSTree, length: *mut u32) -> *mut TSRange; } extern "C" { @@ -375,6 +378,10 @@ extern "C" { length: *mut u32, ) -> *mut TSRange; } +extern "C" { + #[doc = " Write a DOT graph describing the syntax tree to the given file."] + pub fn ts_tree_print_dot_graph(arg1: *const TSTree, file_descriptor: ::std::os::raw::c_int); +} extern "C" { #[doc = " Get the node's type as a null-terminated string."] pub fn ts_node_type(arg1: TSNode) -> *const ::std::os::raw::c_char; diff --git a/lib/binding_rust/lib.rs b/lib/binding_rust/lib.rs index 84471d06..6f044cca 100644 --- a/lib/binding_rust/lib.rs +++ b/lib/binding_rust/lib.rs @@ -775,6 +775,16 @@ impl Tree { result } } + + /// Print a graph of the tree to the given file descriptor. + /// The graph is formatted in the DOT language. You may want to pipe this graph + /// directly to a `dot(1)` process in order to generate SVG output. + #[cfg(unix)] + #[doc(alias = "ts_tree_print_dot_graph")] + pub fn print_dot_graph(&self, file: &impl AsRawFd) { + let fd = file.as_raw_fd(); + unsafe { ffi::ts_tree_print_dot_graph(self.0.as_ptr(), fd) } + } } impl fmt::Debug for Tree { diff --git a/lib/include/tree_sitter/api.h b/lib/include/tree_sitter/api.h index bc05bc3c..5b48cf60 100644 --- a/lib/include/tree_sitter/api.h +++ b/lib/include/tree_sitter/api.h @@ -420,7 +420,7 @@ TSRange *ts_tree_get_changed_ranges( /** * Write a DOT graph describing the syntax tree to the given file. */ -void ts_tree_print_dot_graph(const TSTree *, FILE *); +void ts_tree_print_dot_graph(const TSTree *, int file_descriptor); /******************/ /* Section - Node */ diff --git a/lib/src/clock.h b/lib/src/clock.h index e6faa040..6e75729e 100644 --- a/lib/src/clock.h +++ b/lib/src/clock.h @@ -1,6 +1,7 @@ #ifndef TREE_SITTER_CLOCK_H_ #define TREE_SITTER_CLOCK_H_ +#include #include typedef uint64_t TSDuration; diff --git a/lib/src/tree.c b/lib/src/tree.c index f6bd2c72..bee2a3de 100644 --- a/lib/src/tree.c +++ b/lib/src/tree.c @@ -1,3 +1,4 @@ +#include #include "tree_sitter/api.h" #include "./array.h" #include "./get_changed_ranges.h" @@ -123,6 +124,8 @@ TSRange *ts_tree_get_changed_ranges(const TSTree *self, const TSTree *other, uin return result; } -void ts_tree_print_dot_graph(const TSTree *self, FILE *file) { +void ts_tree_print_dot_graph(const TSTree *self, int fd) { + FILE *file = fdopen(dup(fd), "a"); ts_subtree_print_dot_graph(self->root, self->language, file); + fclose(file); } diff --git a/lib/src/tree.h b/lib/src/tree.h index 0334b824..f012f888 100644 --- a/lib/src/tree.h +++ b/lib/src/tree.h @@ -1,6 +1,8 @@ #ifndef TREE_SITTER_TREE_H_ #define TREE_SITTER_TREE_H_ +#include "./subtree.h" + #ifdef __cplusplus extern "C" { #endif diff --git a/script/generate-bindings b/script/generate-bindings index 4b3fb951..54abac06 100755 --- a/script/generate-bindings +++ b/script/generate-bindings @@ -7,10 +7,7 @@ bindgen \ --no-layout-tests \ --whitelist-type '^TS.*' \ --whitelist-function '^ts_.*' \ - --opaque-type FILE \ - --blocklist-type FILE \ --blocklist-type '^__.*' \ - --blocklist-function ts_tree_print_dot_graph \ --size_t-is-usize \ $header_path > $output_path