rstml/parser/recoverable.rs
1//! Recoverable parser helper module. Contains trait and types that are using
2//! during implementation of parsing with recovery after semantic errors.
3//!
4//! Recoverable parser is a type of parsing technique when parser don't give up
5//! after getting invalid token, and instead continue to parse code to provide
6//! more info about [`TokenStream`] to IDE or user.
7//!
8//! Instead of failing after first unclosed tag, or invalid block, recoverable
9//! parser will try to check if there any other syntax or semantic errors.
10//!
11//! Example:
12//! ```rust
13//! # use quote::quote;
14//! # use rstml::{Parser, ParserConfig};
15//! # Parser::new(ParserConfig::default()).parse_recoverable(quote! {
16//! <div hello={world.} /> // dot after world is invalid syn::Expr
17//! <>
18//! <div>"1"</x> // incorrect closed tag
19//! <div>"2"</div>
20//! <div>"3"</div>
21//! <div {"some-attribute-from-rust-block"}/>
22//! </>
23//! <bar> // unclosed tag
24//! # });
25//! ```
26//! If this example was parsed by regular parser, it will fail with "invalid
27//! expression error" and no output. User will see only one error, and IDE
28//! cannot produce any completion in case of invalid expression.
29//!
30//! But recoverable parser differ (see [`Parser::parse_recoverable`]), it will
31//! return array of errors and array of [`Node`]. Errors should be emitted, and
32//! value should be handled as no errors was found. In result, user will see all
33//! errors, and IDE can provide completion even if some part of token stream was
34//! unexpected.
35//!
36//!
37//! [`TokenStream`]: https://doc.rust-lang.org/proc_macro/struct.TokenStream.html
38//! [`Parser::parse_recoverable`]: struct.Parser.html#method.parse_recoverable
39//! [`Node`]: struct.Node.html
40
41use std::{collections::HashSet, fmt::Debug, rc::Rc};
42
43use proc_macro2_diagnostics::{Diagnostic, Level};
44use syn::parse::{Parse, ParseStream};
45
46use crate::{
47 config::{ElementWildcardFn, TransformBlockFn},
48 node::CustomNode,
49 ParserConfig,
50};
51
52/// Config of parser.
53/// Used to extend parsing functionality by user needs.
54///
55/// Can't be created directly, instead use [`From<ParserConfig>::from`].
56#[derive(Default, Clone)]
57pub struct RecoveryConfig {
58 ///
59 /// Try to parse invalid syn::Block as something.
60 /// Usefull to make expressions more IDE-friendly.
61 pub(crate) recover_block: bool,
62 /// elements that has no child and is always self closed like <img> and <br>
63 pub(crate) always_self_closed_elements: HashSet<&'static str>,
64 /// Elements like `<script>` `<style>`, context of which is not a valid
65 /// html, and should be provided as is.
66 pub(crate) raw_text_elements: HashSet<&'static str>,
67 pub(crate) transform_block: Option<Rc<TransformBlockFn>>,
68 /// Allows wildcard closing tag matching for blocks
69 pub(crate) element_close_wildcard: Option<Rc<ElementWildcardFn>>,
70}
71impl PartialEq for RecoveryConfig {
72 fn eq(&self, other: &Self) -> bool {
73 self.recover_block == other.recover_block
74 && self.always_self_closed_elements == other.always_self_closed_elements
75 && self.raw_text_elements == other.raw_text_elements
76 && self.transform_block.is_some() == other.transform_block.is_some()
77 && self.element_close_wildcard.is_some() == other.element_close_wildcard.is_some()
78 }
79}
80impl Eq for RecoveryConfig {}
81
82impl Debug for RecoveryConfig {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.debug_struct("RecoveryConfig")
85 .field("recover_block", &self.recover_block)
86 .field(
87 "always_self_closed_elements",
88 &self.always_self_closed_elements,
89 )
90 .field("raw_text_elements", &self.raw_text_elements)
91 .field(
92 "element_close_wildcard",
93 &self.element_close_wildcard.is_some(),
94 )
95 .finish()
96 }
97}
98
99/// Context that is provided in [`ParseRecoverable`] interface.
100/// Used to save [`Diagnostic`] messages or [`syn::Result`].
101///
102/// Also can be extended with user needs through [`RecoveryConfig`].
103#[derive(Debug, Default, Clone)]
104pub struct RecoverableContext {
105 pub(super) diagnostics: Vec<Diagnostic>,
106 config: RecoveryConfig,
107}
108
109impl PartialEq for RecoverableContext {
110 fn eq(&self, other: &Self) -> bool {
111 if self.diagnostics.len() != other.diagnostics.len() || self.config != other.config {
112 return false;
113 }
114
115 self.diagnostics
116 .iter()
117 .zip(other.diagnostics.iter())
118 .all(|(a, b)| format!("{:?}", a) == format!("{:?}", b))
119 }
120}
121impl Eq for RecoverableContext {}
122
123impl RecoverableContext {
124 pub fn new(config: RecoveryConfig) -> Self {
125 Self {
126 diagnostics: vec![],
127 config,
128 }
129 }
130 pub fn config(&self) -> &RecoveryConfig {
131 &self.config
132 }
133 pub fn parse_result<T>(self, val: Option<T>) -> ParsingResult<T> {
134 ParsingResult::from_parts(val, self.diagnostics)
135 }
136
137 /// Parse token using [`syn::parse::Parse`]
138 pub fn parse_simple<T: Parse>(&mut self, input: ParseStream) -> Option<T> {
139 match input.parse() {
140 Ok(v) => Some(v),
141 Err(e) => {
142 self.push_diagnostic(e);
143 None
144 }
145 }
146 }
147 /// Parse token using closure
148 pub fn parse_mixed_fn<F, T>(&mut self, input: ParseStream, mut parser: F) -> Option<T>
149 where
150 F: FnMut(&mut Self, ParseStream) -> Result<T, syn::Error>,
151 {
152 match parser(self, input) {
153 Ok(v) => Some(v),
154 Err(e) => {
155 self.push_diagnostic(e);
156 None
157 }
158 }
159 }
160
161 /// Parse token using [`ParseRecoverable`]
162 pub fn parse_recoverable<T: ParseRecoverable>(&mut self, input: ParseStream) -> Option<T> {
163 T::parse_recoverable(self, input)
164 }
165
166 /// Save diagnostic message of [`syn::Result`]
167 /// and convert result to `Option`, that can be used directly
168 /// as output in [`ParseRecoverable::parse_recoverable`]
169 pub fn save_diagnostics<T>(&mut self, val: syn::Result<T>) -> Option<T> {
170 match val {
171 Ok(v) => Some(v),
172 Err(e) => {
173 self.push_diagnostic(e);
174 None
175 }
176 }
177 }
178
179 /// Push custom message of [`syn::Error`] or
180 /// [`proc_macro2_diagnostics::Diagnostic`]
181 pub fn push_diagnostic(&mut self, diagnostic: impl Into<Diagnostic>) {
182 let diag = diagnostic.into();
183 // println!(
184 // "Push diagnostic: {:?}, backtrace={}",
185 // diag,
186 // std::backtrace::Backtrace::capture()
187 // );
188 self.diagnostics.push(diag);
189 }
190}
191
192///
193/// Result of parsing.
194#[derive(Debug)]
195pub enum ParsingResult<T> {
196 /// Fully valid ast that was parsed without errors.
197 Ok(T),
198 /// The ast contain invalid starting tokens, and cannot be parsed.
199 Failed(Vec<Diagnostic>),
200 /// The ast can be partially parsed,
201 /// but some tokens was skipped during parsing, or their meaning was
202 /// changed.
203 Partial(T, Vec<Diagnostic>),
204}
205
206impl<T> ParsingResult<T> {
207 /// Create new ParsingResult from optional value and accumulated errors.
208 pub fn from_parts(value: Option<T>, errors: Vec<Diagnostic>) -> Self {
209 match (value, errors) {
210 (Some(v), err) if err.is_empty() => Self::Ok(v),
211 (Some(v), err) => Self::Partial(v, err),
212 (None, err) => Self::Failed(err),
213 }
214 }
215
216 ///
217 /// Convert into [`syn::Result`], with fail on first diagnostic message,
218 /// Ignores any diagnostic non error message when result is available.
219 /// Returns Error on [`ParsingResult::Failed`], and
220 /// [`ParsingResult::Partial`].
221 pub fn into_result(self) -> syn::Result<T> {
222 match self {
223 ParsingResult::Ok(r) => Ok(r),
224 ParsingResult::Failed(errors) => Err(errors
225 .into_iter()
226 .next()
227 .unwrap_or_else(|| {
228 Diagnostic::new(
229 Level::Error,
230 "Object parsing failed, but no additional info was provided",
231 )
232 })
233 .into()),
234 ParsingResult::Partial(ok, errors) => {
235 if let Some(err) = errors
236 .into_iter()
237 .filter(|p| p.level() == Level::Error)
238 .next()
239 {
240 Err(err.into())
241 } else {
242 Ok(ok)
243 }
244 }
245 }
246 }
247
248 pub fn split(self) -> (Option<T>, Vec<Diagnostic>) {
249 match self {
250 Self::Ok(r) => (Some(r), vec![]),
251 Self::Failed(errors) => (None, errors),
252 Self::Partial(r, errors) => (Some(r), errors),
253 }
254 }
255
256 pub fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
257 *self = match std::mem::replace(self, ParsingResult::Failed(vec![])) {
258 Self::Ok(r) => Self::Partial(r, vec![diagnostic]),
259 Self::Failed(errors) => {
260 Self::Failed(errors.into_iter().chain(Some(diagnostic)).collect())
261 }
262 Self::Partial(r, errors) => {
263 Self::Partial(r, errors.into_iter().chain(Some(diagnostic)).collect())
264 }
265 };
266 }
267
268 pub fn is_ok(&self) -> bool {
269 matches!(self, Self::Ok(_))
270 }
271}
272
273impl<T> ParsingResult<Vec<T>> {
274 pub fn split_vec(self) -> (Vec<T>, Vec<Diagnostic>) {
275 let (r, e) = self.split();
276 (r.unwrap_or_default(), e)
277 }
278 pub fn from_parts_vec(value: Vec<T>, errors: Vec<Diagnostic>) -> Self {
279 match (value, errors) {
280 (v, err) if err.is_empty() => Self::Ok(v),
281 (v, err) if !v.is_empty() => Self::Partial(v, err),
282 (_, err) => Self::Failed(err),
283 }
284 }
285}
286
287impl<T> From<syn::Result<T>> for ParsingResult<T> {
288 ///
289 /// Convert into syn::Result,
290 /// Returns Error on ParsingResult::Failed, and ParsingResult::Partial.
291 fn from(result: syn::Result<T>) -> ParsingResult<T> {
292 match result {
293 Result::Ok(r) => ParsingResult::Ok(r),
294 Result::Err(e) => ParsingResult::Failed(vec![e.into()]),
295 }
296 }
297}
298
299impl<C: CustomNode> From<ParserConfig<C>> for RecoveryConfig {
300 fn from(config: ParserConfig<C>) -> Self {
301 RecoveryConfig {
302 recover_block: config.recover_block,
303 raw_text_elements: config.raw_text_elements.clone(),
304 always_self_closed_elements: config.always_self_closed_elements.clone(),
305 transform_block: config.transform_block.clone(),
306 element_close_wildcard: config.element_close_wildcard.clone(),
307 }
308 }
309}
310
311///
312/// Adaptor to provide a [`syn::parse::Parse`] interface to [`ParseRecoverable`]
313/// types. Returns error if any error was set in [`RecoverableContext`] during
314/// parsing. Use Default implementation of [`RecoveryConfig`].
315///
316/// Panics:
317/// If [`ParseRecoverable`] implementation doesn't save any diagnostic message,
318/// and return [`None`].
319pub struct Recoverable<T>(pub T);
320impl<T> Recoverable<T> {
321 pub fn inner(self) -> T {
322 self.0
323 }
324}
325
326impl<T: ParseRecoverable> Parse for Recoverable<T> {
327 fn parse(input: ParseStream) -> syn::Result<Self> {
328 let mut empty_context = RecoverableContext::default();
329 let parse = T::parse_recoverable(&mut empty_context, input);
330 empty_context
331 .parse_result(parse)
332 .into_result()
333 .map(Recoverable)
334 }
335}
336
337///
338/// Parsing interface for recoverable [`TokenStream`] parsing,
339/// analog to [`syn::parse::Parse`] but with ability to skip unexpected
340/// tokens, and more diagnostic messages.
341///
342/// - If input stream can be parsed to valid, or partially valid object
343/// [`Option::Some`] should be returned.
344///
345/// - If object is parsed partially one can save
346/// diagnostic message in [`RecoverableContext`].
347///
348/// - If object is failed to parse
349/// [`Option::None`] should be returned, and any message should be left in
350/// [`RecoverableContext`].
351///
352/// Instead of using [`RecoverableContext`] the interface can be changed to the
353/// following:
354/// ```rust
355/// # use syn::parse::ParseStream;
356/// # use rstml::ParsingResult;
357/// pub trait ParseRecoverable: Sized {
358/// fn parse_recoverable(input: ParseStream) -> ParsingResult<Self>;
359/// }
360/// ```
361/// It would more type-safe, but because [`std::ops::Try`] is not stable,
362/// writing implementation for this trait would end with a lot of boilerplate.
363///
364/// [`TokenStream`]: https://doc.rust-lang.org/proc_macro/struct.TokenStream.html
365pub trait ParseRecoverable: Sized {
366 fn parse_recoverable(parser: &mut RecoverableContext, input: ParseStream) -> Option<Self>;
367}