Skip to content

Commit 57564fc

Browse files
committed
feat: adding proper error backtrace and display
1 parent 54f5686 commit 57564fc

File tree

6 files changed

+83
-36
lines changed

6 files changed

+83
-36
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/target
22
/Cargo.lock
3+
/benchmark

src/error.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ pub type Result<T> = std::result::Result<T, RenderError>;
55

66
#[derive(Error, Debug, PartialEq)]
77
pub enum RenderError {
8-
#[error("Syntax error")]
8+
#[error("Syntax error inside command")]
99
SyntaxError,
10-
#[error("Function error")]
10+
#[error("Template function call error")]
1111
FunctionError,
12-
#[error("Missing command tag")]
13-
MissingCommandTag,
14-
#[error("Missing closing tag")]
15-
MissingClosingTag,
12+
#[error("Missing command type at `{0}`")]
13+
MissingCommandType(String),
14+
#[error("Missing closing command tag at `{0}`")]
15+
MissingClosingTag(String),
1616
}
1717

1818
impl Into<JsValue> for RenderError {

src/lib.rs

+57-20
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use std::collections::HashMap;
77
use std::fmt::Write;
88

99
use serde::{Deserialize, Serialize};
10+
use utils::count_newlines;
1011
use wasm_bindgen::prelude::*;
1112

1213
use crate::error::RenderError;
1314
use crate::error::Result;
14-
use crate::utils::log;
1515

1616
#[derive(Debug, PartialEq)]
1717
pub enum CommandType {
@@ -39,6 +39,11 @@ pub enum Token<'a> {
3939
Command(Command<'a>),
4040
}
4141

42+
pub struct ParsingData {
43+
line: usize,
44+
ch: usize,
45+
}
46+
4247
#[wasm_bindgen]
4348
#[derive(Serialize, Deserialize, Debug)]
4449
pub struct ParserConfig {
@@ -115,35 +120,56 @@ impl<'a> Parser<'a> {
115120
Parser { content, config }
116121
}
117122

118-
fn parse_command_tag(&self, i: &'a str) -> Result<(CommandType, &'a str)> {
123+
fn generate_backtrace(&self, d: &ParsingData) -> String {
124+
let line = self.content.lines().skip(d.line as usize).next().unwrap();
125+
let line_ch: usize = self
126+
.content
127+
.split('\n')
128+
.take(d.line as usize)
129+
.map(|l| l.len()+1)
130+
.sum();
131+
let ch = d.ch - line_ch;
132+
let mut spaces: String = (0..ch-1).map(|_| ' ').collect();
133+
spaces += "^";
134+
let s = format!("line {} col {}:\n\n{}\n{}", d.line + 1, ch, line, spaces);
135+
s
136+
}
137+
138+
fn parse_command_tag(&self, i: &'a str, d: &mut ParsingData) -> Result<(CommandType, &'a str)> {
119139
let c = i.chars().next();
120140
let c = match c {
121141
Some(c) => c,
122-
None => return Err(RenderError::MissingCommandTag),
142+
None => return Err(RenderError::MissingCommandType(self.generate_backtrace(d))),
123143
};
124144

125145
// TODO: improve this
126146
let mut input = i;
127147
let cmd_type = if c == self.config.execution {
128148
input = &i[1..];
149+
d.ch += 1;
129150
CommandType::Execution
130151
} else if c == self.config.interpolate {
131152
input = &i[1..];
153+
d.ch += 1;
132154
CommandType::Interpolate
133155
} else {
134156
if self.config.interpolate == '\0' {
135157
CommandType::Interpolate
136158
} else if self.config.execution == '\0' {
137159
CommandType::Execution
138160
} else {
139-
return Err(RenderError::MissingCommandTag);
161+
return Err(RenderError::MissingCommandType(self.generate_backtrace(d)));
140162
}
141163
};
142164

143165
Ok((cmd_type, input))
144166
}
145167

146-
fn parse_whitespace(&self, i: &'a str) -> Result<(Option<Whitespace>, &'a str)> {
168+
fn parse_whitespace(
169+
&self,
170+
i: &'a str,
171+
d: &mut ParsingData,
172+
) -> Result<(Option<Whitespace>, &'a str)> {
147173
let c = i.chars().next();
148174
let whitespace = match c {
149175
Some(c) => {
@@ -160,15 +186,17 @@ impl<'a> Parser<'a> {
160186
let mut input = i;
161187
if whitespace.is_some() {
162188
input = &i[1..];
189+
d.ch += 1;
163190
}
164191
Ok((whitespace, input))
165192
}
166193

167-
fn parse_closing_tag(&self, i: &'a str) -> Result<(&'a str, &'a str)> {
194+
fn parse_closing_tag(&self, i: &'a str, d: &mut ParsingData) -> Result<(&'a str, &'a str)> {
168195
let (content, i) = match i.split_once(&self.config.closing_tag) {
169196
Some(x) => x,
170-
None => return Err(RenderError::MissingClosingTag),
197+
None => return Err(RenderError::MissingClosingTag(self.generate_backtrace(d))),
171198
};
199+
d.ch += self.config.closing_tag.len();
172200
Ok((content, i))
173201
}
174202

@@ -186,7 +214,7 @@ impl<'a> Parser<'a> {
186214
res.iter().collect()
187215
}
188216

189-
pub fn trim_whitespace<'b>(
217+
fn trim_whitespace<'b>(
190218
&self,
191219
i: &'b str,
192220
whitespace: Option<&Whitespace>,
@@ -210,7 +238,7 @@ impl<'a> Parser<'a> {
210238
}
211239
}
212240
x
213-
},
241+
}
214242
Some('\r') => {
215243
let mut x = 1;
216244
if left {
@@ -245,25 +273,34 @@ impl<'a> Parser<'a> {
245273
pub fn parse_tokens(&self) -> Result<Vec<Token>> {
246274
let mut tokens = vec![];
247275
let mut input = self.content;
276+
let mut parsing_data = ParsingData { ch: 0, line: 0 };
248277

249278
while let Some((text, i)) = input.split_once(&self.config.opening_tag) {
279+
parsing_data.ch += self.config.opening_tag.len();
280+
250281
if !text.is_empty() {
251282
tokens.push(Token::Text(text));
283+
parsing_data.ch += text.len();
284+
parsing_data.line += count_newlines(text);
252285
}
253286

254-
let (cmd_type, i) = self.parse_command_tag(i)?;
255-
let (opening_whitespace, i) = self.parse_whitespace(i)?;
256-
let (part1, i) = self.parse_closing_tag(i)?;
287+
let (opening_whitespace, i) = self.parse_whitespace(i, &mut parsing_data)?;
288+
let (cmd_type, i) = self.parse_command_tag(i, &mut parsing_data)?;
289+
let (part1, i) = self.parse_closing_tag(i, &mut parsing_data)?;
257290

258291
// TODO: improve that
259292
let content_whitespace = &part1[part1.len() - 1..];
260-
let (closing_whitespace, _) = self.parse_whitespace(content_whitespace)?;
293+
let (closing_whitespace, _) =
294+
self.parse_whitespace(content_whitespace, &mut parsing_data)?;
261295
let content = if closing_whitespace.is_none() {
262296
part1
263297
} else {
264298
&part1[..part1.len() - 1]
265299
};
266300

301+
parsing_data.ch += content.len();
302+
parsing_data.line += count_newlines(content);
303+
267304
let command = Command {
268305
r#type: cmd_type,
269306
opening_whitespace,
@@ -276,6 +313,8 @@ impl<'a> Parser<'a> {
276313
}
277314
if !input.is_empty() {
278315
tokens.push(Token::Text(input));
316+
parsing_data.ch += input.len();
317+
parsing_data.line += count_newlines(input);
279318
}
280319

281320
Ok(tokens)
@@ -362,13 +401,11 @@ impl Renderer {
362401
let parser = Parser::new(content, &self.config);
363402
let tokens = parser.parse_tokens()?;
364403
let fn_body = parser.generate_js(tokens);
365-
let async_fn = match self
366-
.async_constructor
367-
.call2(
368-
&JsValue::NULL,
369-
&JsValue::from("tp"),
370-
&JsValue::from(&fn_body).into(),
371-
) {
404+
let async_fn = match self.async_constructor.call2(
405+
&JsValue::NULL,
406+
&JsValue::from("tp"),
407+
&JsValue::from(&fn_body).into(),
408+
) {
372409
Ok(f) => f,
373410
Err(_) => return Err(RenderError::SyntaxError),
374411
};

src/utils.rs

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
pub fn count_newlines(s: &str) -> usize {
2+
s.as_bytes().iter().filter(|&c| *c == b'\n').count()
3+
}
14

25
macro_rules! log {
36
( $( $t:tt )* ) => {

tests/error.rs

+13-7
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,30 @@ use rusty_engine::{error::RenderError, Parser, ParserConfig, Renderer};
99
#[wasm_bindgen_test]
1010
pub fn test_missing_closing_tag() {
1111
let config = ParserConfig::new("<%".into(), "%>".into(), '\0', '*', '-', '_', "tR".into());
12-
let content = r#"test
13-
<%_ test -%>
14-
test
15-
<% test"#;
12+
let content = "test\r\n<%_ test1 -%>\r\ntest\r\ntesttest <% test2";
13+
let err = r#"line 4 col 11:
14+
15+
testtest <% test2
16+
^"#;
1617

1718
let parser = Parser::new(content, &config);
1819
let tokens = parser.parse_tokens();
19-
assert_eq!(tokens, Err(RenderError::MissingClosingTag));
20+
assert_eq!(tokens, Err(RenderError::MissingClosingTag(err.into())));
2021
}
2122

2223
#[wasm_bindgen_test]
2324
pub fn test_missing_command_tag() {
2425
let config = ParserConfig::new("<%".into(), "%>".into(), '~', '*', '-', '_', "tR".into());
25-
let content = r#"test <% test %> test"#;
26+
let content = r#"test
27+
test <% test %> test"#;
28+
let err = r#"line 2 col 7:
29+
30+
test <% test %> test
31+
^"#;
2632

2733
let parser = Parser::new(content, &config);
2834
let tokens = parser.parse_tokens();
29-
assert_eq!(tokens, Err(RenderError::MissingCommandTag));
35+
assert_eq!(tokens, Err(RenderError::MissingCommandType(err.into())));
3036
}
3137

3238
#[wasm_bindgen_test]

tests/web.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub fn test_parse_tokens() {
2020
test<%_ test %>test
2121
<%- test _%>
2222
test
23-
<%*_ test -%> test <% test %>
23+
<%_* test -%> test <% test %>
2424
test"#;
2525

2626
let parser = Parser::new(content, &config);
@@ -73,7 +73,7 @@ pub fn test_generate_js() {
7373
'_',
7474
"tR".into(),
7575
);
76-
let content = "<%- test -%>\ntest\n\n<%*_ test %>'test'<% test %>";
76+
let content = "<%- test -%>\ntest\n\n<%_* test %>'test'<% test %>";
7777
let parser = Parser::new(content, &config);
7878
let tokens = parser.parse_tokens().unwrap();
7979
let js_func = parser.generate_js(tokens);
@@ -108,7 +108,7 @@ pub fn test_whitespace_control() {
108108
'_',
109109
"tR".into(),
110110
);
111-
let content = "\ntest\n\n<%_ test -%>\r\n\ntest\n\r<%*- test _%>\rtest\r\n<%*- test -%> test <% test %>\ntest";
111+
let content = "\ntest\n\n<%_ test -%>\r\n\ntest\n\r<%-* test _%>\rtest\r\n<%-* test -%> test <% test %>\ntest";
112112

113113
let parser = Parser::new(content, &config);
114114
let tokens = parser.parse_tokens().unwrap();

0 commit comments

Comments
 (0)