fork of https://github.com/tree-sitter/tree-sitter-graph

Merge pull request #125 from tree-sitter/pretty-parse-errors

authored by Hendrik van Antwerpen and committed by GitHub e441d607 3d901dbd

Changed files
+226 -148
src
bin
tree-sitter-graph
execution
+14
CHANGELOG.md
··· 5 5 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 6 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 7 8 + ## Unreleased 9 + 10 + ### Library 11 + 12 + #### Changed 13 + 14 + - The method `ParseError::display(source: &str, verbose: bool)` has been replaced by two methods `ParseError::display(path: &path, source: &str)` and `ParseError::display_pretty(path: &Path, source: &str)`. 15 + 16 + ### CLI 17 + 18 + #### Changed 19 + 20 + - Parse errors are now displayed with the same excerpt style as execution errors. 21 + 8 22 ## 0.8.0 -- 2023-03-29 9 23 10 24 ### Library
+2 -10
src/bin/tree-sitter-graph/main.rs
··· 114 114 let parse_errors = ParseError::all(&tree); 115 115 if !parse_errors.is_empty() { 116 116 for parse_error in parse_errors.iter().take(MAX_PARSE_ERRORS) { 117 - let line = parse_error.node().start_position().row; 118 - let column = parse_error.node().start_position().column; 119 - eprintln!( 120 - "{}:{}:{}: {}", 121 - source_path.display(), 122 - line + 1, 123 - column + 1, 124 - parse_error.display(&source, true) 125 - ); 117 + eprintln!("{}", parse_error.display_pretty(source_path, &source)); 126 118 } 127 119 if parse_errors.len() > MAX_PARSE_ERRORS { 128 120 let more_errors = parse_errors.len() - MAX_PARSE_ERRORS; ··· 141 133 let graph = match file.execute(&tree, &source, &mut config, &NoCancellation) { 142 134 Ok(graph) => graph, 143 135 Err(e) => { 144 - eprint!("{}", e.display_pretty(source_path, &source, tsg_path, &tsg)); 136 + eprintln!("{}", e.display_pretty(source_path, &source, tsg_path, &tsg)); 145 137 return Err(anyhow!("Cannot execute TSG file {}", tsg_path.display())); 146 138 } 147 139 };
+24 -102
src/execution/error.rs
··· 5 5 // Please see the LICENSE-APACHE or LICENSE-MIT files in this distribution for license details. 6 6 // ------------------------------------------------------------------------------------------------ 7 7 8 - #[cfg(feature = "term-colors")] 9 - use colored::Colorize; 10 8 use std::path::Path; 11 9 use thiserror::Error; 12 10 13 - use crate::ast::{Stanza, Statement}; 11 + use crate::ast::Stanza; 12 + use crate::ast::Statement; 14 13 use crate::execution::CancellationError; 14 + use crate::parse_error::Excerpt; 15 15 use crate::Location; 16 16 17 17 /// An error that can occur while executing a graph DSL file ··· 213 213 write!( 214 214 f, 215 215 "{}", 216 - Excerpt::from_source(self.tsg_path, self.tsg, statement_location, 7) 216 + Excerpt::from_source( 217 + self.tsg_path, 218 + self.tsg, 219 + statement_location.row, 220 + statement_location.to_column_range(), 221 + 7 222 + ) 217 223 )?; 218 224 writeln!(f, "{}in stanza", " ".repeat(7))?; 219 225 write!( 220 226 f, 221 227 "{}", 222 - Excerpt::from_source(self.tsg_path, self.tsg, stanza_location, 7) 228 + Excerpt::from_source( 229 + self.tsg_path, 230 + self.tsg, 231 + stanza_location.row, 232 + stanza_location.to_column_range(), 233 + 7 234 + ) 223 235 )?; 224 236 writeln!(f, "{}matching ({}) node", " ".repeat(7), node_kind)?; 225 237 write!( 226 238 f, 227 239 "{}", 228 - Excerpt::from_source(self.source_path, self.source, source_location, 7) 240 + Excerpt::from_source( 241 + self.source_path, 242 + self.source, 243 + source_location.row, 244 + source_location.to_column_range(), 245 + 7 246 + ) 229 247 )?; 230 248 Ok(()) 231 249 } ··· 237 255 } 238 256 } 239 257 } 240 - 241 - /// Excerpts of source from either the target language file or the tsg rules file. 242 - struct Excerpt<'a> { 243 - path: &'a Path, 244 - source: Option<&'a str>, 245 - location: &'a Location, 246 - indent: usize, 247 - } 248 - 249 - impl<'a> Excerpt<'a> { 250 - pub fn from_source( 251 - path: &'a Path, 252 - source: &'a str, 253 - location: &'a Location, 254 - indent: usize, 255 - ) -> Excerpt<'a> { 256 - Excerpt { 257 - path, 258 - source: source.lines().nth(location.row), 259 - location, 260 - indent, 261 - } 262 - } 263 - 264 - fn gutter_width(&self) -> usize { 265 - ((self.location.row + 1) as f64).log10() as usize + 1 266 - } 267 - } 268 - 269 - impl<'a> std::fmt::Display for Excerpt<'a> { 270 - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 271 - // path and line/col 272 - write!( 273 - f, 274 - "{}{}:{}:{}:", 275 - " ".repeat(self.indent), 276 - white_bold(&self.path.to_str().unwrap_or("<unknown file>")), 277 - white_bold(&format!("{}", self.location.row + 1)), 278 - white_bold(&format!("{}", self.location.column + 1)), 279 - )?; 280 - if let Some(source) = self.source { 281 - writeln!(f)?; 282 - // first line: line number & source 283 - writeln!( 284 - f, 285 - "{}{}{}{}", 286 - " ".repeat(self.indent), 287 - blue(&format!("{}", self.location.row + 1)), 288 - blue(" | "), 289 - source, 290 - )?; 291 - // second line: caret 292 - writeln!( 293 - f, 294 - "{}{}{}{}{}", 295 - " ".repeat(self.indent), 296 - " ".repeat(self.gutter_width()), 297 - blue(" | "), 298 - " ".repeat(self.location.column), 299 - green_bold("^") 300 - )?; 301 - } else { 302 - writeln!(f, " <missing source>")?; 303 - } 304 - Ok(()) 305 - } 306 - } 307 - 308 - // coloring functions 309 - 310 - #[cfg(feature = "term-colors")] 311 - fn blue(str: &str) -> impl std::fmt::Display { 312 - str.blue() 313 - } 314 - #[cfg(not(feature = "term-colors"))] 315 - fn blue<'a>(str: &'a str) -> impl std::fmt::Display + 'a { 316 - str 317 - } 318 - 319 - #[cfg(feature = "term-colors")] 320 - fn green_bold(str: &str) -> impl std::fmt::Display { 321 - str.green().bold() 322 - } 323 - #[cfg(not(feature = "term-colors"))] 324 - fn green_bold<'a>(str: &'a str) -> impl std::fmt::Display + 'a { 325 - str 326 - } 327 - 328 - #[cfg(feature = "term-colors")] 329 - fn white_bold(str: &str) -> impl std::fmt::Display { 330 - str.white().bold() 331 - } 332 - #[cfg(not(feature = "term-colors"))] 333 - fn white_bold<'a>(str: &'a str) -> impl std::fmt::Display + 'a { 334 - str 335 - }
+181 -36
src/parse_error.rs
··· 7 7 8 8 //! Data types and functions for finding and displaying tree-sitter parse errors. 9 9 10 + #[cfg(feature = "term-colors")] 11 + use colored::Colorize; 12 + use std::ops::Range; 13 + use std::path::Path; 10 14 use tree_sitter::Node; 11 15 use tree_sitter::Tree; 12 16 ··· 57 61 } 58 62 } 59 63 60 - pub fn display(&self, source: &'tree str, verbose: bool) -> ParseErrorDisplay { 64 + pub fn display( 65 + &'tree self, 66 + path: &'tree Path, 67 + source: &'tree str, 68 + ) -> impl std::fmt::Display + 'tree { 61 69 ParseErrorDisplay { 62 70 error: self, 71 + path, 63 72 source, 64 - verbose, 73 + } 74 + } 75 + 76 + pub fn display_pretty( 77 + &'tree self, 78 + path: &'tree Path, 79 + source: &'tree str, 80 + ) -> impl std::fmt::Display + 'tree { 81 + ParseErrorDisplayPretty { 82 + error: self, 83 + path, 84 + source, 65 85 } 66 86 } 67 87 } 68 88 69 - pub struct ParseErrorDisplay<'tree> { 89 + struct ParseErrorDisplay<'tree> { 70 90 error: &'tree ParseError<'tree>, 91 + path: &'tree Path, 71 92 source: &'tree str, 72 - verbose: bool, 73 93 } 74 94 75 95 impl std::fmt::Display for ParseErrorDisplay<'_> { 76 96 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 97 + let node = self.error.node(); 98 + write!( 99 + f, 100 + "{}:{}:{}: ", 101 + self.path.display(), 102 + node.start_position().row + 1, 103 + node.start_position().column + 1 104 + )?; 77 105 let node = match self.error { 78 106 ParseError::Missing(node) => { 79 - write!(f, "Missing syntax")?; 107 + write!(f, "missing syntax")?; 80 108 node 81 109 } 82 110 ParseError::Unexpected(node) => { 83 - write!(f, "Unexpected syntax")?; 111 + write!(f, "unexpected syntax")?; 84 112 node 85 113 } 86 114 }; 87 - let line = node.start_position().row; 88 - let start_column = node.start_position().column; 89 115 if node.byte_range().is_empty() { 90 116 writeln!(f, "")?; 91 117 } else { 92 - let (end_column, end_byte) = self.source[node.byte_range()] 118 + let end_byte = self.source[node.byte_range()] 93 119 .chars() 94 120 .take_while(|c| *c != '\n') 95 - .fold( 96 - (node.start_position().column, node.start_byte()), 97 - |(column, byte), c| (column + 1, byte + c.len_utf8()), 98 - ); 99 - if !self.verbose { 100 - let text = &self.source[node.start_byte()..end_byte]; 101 - write!(f, ": {}", text)?; 102 - } else { 103 - let text = self 104 - .source 105 - .lines() 106 - .nth(line) 107 - .expect("parse error has invalid row"); 108 - writeln!(f, ":")?; 109 - writeln!(f, "")?; 110 - writeln!(f, "| {}", text)?; 111 - write!( 112 - f, 113 - " {}{}", 114 - " ".repeat(start_column), 115 - "^".repeat(end_column - start_column) 116 - )?; 117 - if node.end_position().row == line { 118 - writeln!(f, "")?; 119 - } else { 120 - writeln!(f, "...")?; 121 - } 121 + .map(|c| c.len_utf8()) 122 + .sum(); 123 + let text = &self.source[node.start_byte()..end_byte]; 124 + write!(f, ": {}", text)?; 125 + } 126 + Ok(()) 127 + } 128 + } 129 + 130 + struct ParseErrorDisplayPretty<'tree> { 131 + error: &'tree ParseError<'tree>, 132 + path: &'tree Path, 133 + source: &'tree str, 134 + } 135 + 136 + impl std::fmt::Display for ParseErrorDisplayPretty<'_> { 137 + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 138 + let node = match self.error { 139 + ParseError::Missing(node) => { 140 + writeln!(f, "missing syntax")?; 141 + node 122 142 } 143 + ParseError::Unexpected(node) => { 144 + writeln!(f, "unexpected syntax")?; 145 + node 146 + } 147 + }; 148 + if node.byte_range().is_empty() { 149 + writeln!(f, "")?; 150 + } else { 151 + let start_column = node.start_position().column; 152 + let end_column = node.start_position().column 153 + + self.source[node.byte_range()] 154 + .chars() 155 + .take_while(|c| *c != '\n') 156 + .count(); 157 + write!( 158 + f, 159 + "{}", 160 + Excerpt::from_source( 161 + self.path, 162 + self.source, 163 + node.start_position().row, 164 + start_column..end_column, 165 + 0, 166 + ), 167 + )?; 123 168 } 124 169 Ok(()) 125 170 } ··· 308 353 // This is okay because Send and Sync _are_ implemented for Tree, which also holds ffi::TSTree 309 354 unsafe impl Send for TreeWithParseErrorVec {} 310 355 unsafe impl Sync for TreeWithParseErrorVec {} 356 + 357 + //----------------------------------------------------------------------------- 358 + 359 + /// Excerpts of source from either the target language file or the tsg rules file. 360 + pub(crate) struct Excerpt<'a> { 361 + path: &'a Path, 362 + source: Option<&'a str>, 363 + row: usize, 364 + columns: Range<usize>, 365 + indent: usize, 366 + } 367 + 368 + impl<'a> Excerpt<'a> { 369 + pub fn from_source( 370 + path: &'a Path, 371 + source: &'a str, 372 + row: usize, 373 + columns: Range<usize>, 374 + indent: usize, 375 + ) -> Excerpt<'a> { 376 + Excerpt { 377 + path, 378 + source: source.lines().nth(row), 379 + row, 380 + columns, 381 + indent, 382 + } 383 + } 384 + 385 + fn gutter_width(&self) -> usize { 386 + ((self.row + 1) as f64).log10() as usize + 1 387 + } 388 + } 389 + 390 + impl<'a> std::fmt::Display for Excerpt<'a> { 391 + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 392 + // path and line/col 393 + writeln!( 394 + f, 395 + "{}{}:{}:{}:", 396 + " ".repeat(self.indent), 397 + white_bold(&self.path.to_str().unwrap_or("<unknown file>")), 398 + white_bold(&format!("{}", self.row + 1)), 399 + white_bold(&format!("{}", self.columns.start + 1)), 400 + )?; 401 + if let Some(source) = self.source { 402 + // first line: line number & source 403 + writeln!( 404 + f, 405 + "{}{}{}{}", 406 + " ".repeat(self.indent), 407 + blue(&format!("{}", self.row + 1)), 408 + blue(" | "), 409 + source, 410 + )?; 411 + // second line: caret 412 + writeln!( 413 + f, 414 + "{}{}{}{}{}", 415 + " ".repeat(self.indent), 416 + " ".repeat(self.gutter_width()), 417 + blue(" | "), 418 + " ".repeat(self.columns.start), 419 + green_bold(&"^".repeat(self.columns.len())) 420 + )?; 421 + } else { 422 + writeln!(f, "{}{}", " ".repeat(self.indent), "<missing source>",)?; 423 + } 424 + Ok(()) 425 + } 426 + } 427 + 428 + // coloring functions 429 + 430 + #[cfg(feature = "term-colors")] 431 + fn blue(str: &str) -> impl std::fmt::Display { 432 + str.blue() 433 + } 434 + #[cfg(not(feature = "term-colors"))] 435 + fn blue<'a>(str: &'a str) -> impl std::fmt::Display + 'a { 436 + str 437 + } 438 + 439 + #[cfg(feature = "term-colors")] 440 + fn green_bold(str: &str) -> impl std::fmt::Display { 441 + str.green().bold() 442 + } 443 + #[cfg(not(feature = "term-colors"))] 444 + fn green_bold<'a>(str: &'a str) -> impl std::fmt::Display + 'a { 445 + str 446 + } 447 + 448 + #[cfg(feature = "term-colors")] 449 + fn white_bold(str: &str) -> impl std::fmt::Display { 450 + str.white().bold() 451 + } 452 + #[cfg(not(feature = "term-colors"))] 453 + fn white_bold<'a>(str: &'a str) -> impl std::fmt::Display + 'a { 454 + str 455 + }
+5
src/parser.rs
··· 7 7 8 8 use std::fmt::Display; 9 9 use std::iter::Peekable; 10 + use std::ops::Range; 10 11 use std::str::Chars; 11 12 12 13 use regex::Regex; ··· 91 92 } else { 92 93 self.column += 1; 93 94 } 95 + } 96 + 97 + pub(crate) fn to_column_range(&self) -> Range<usize> { 98 + self.column..self.column + 1 94 99 } 95 100 } 96 101