Repository: djc/askama
Pull request #1011 (2024-04-18 02:36:42)

Implement `Render` even if parsing failed
This makes error messages much more readable.


Patch:
diff --git a/askama_derive/src/heritage.rs b/askama_derive/src/heritage.rs
index 2d70c08..76894cb 100644
--- a/askama_derive/src/heritage.rs
+++ b/askama_derive/src/heritage.rs
@@ -36,7 +36,7 @@ impl Heritage<'_> {
 
 type BlockAncestry<'a> = HashMap<&'a str, Vec<(&'a Context<'a>, &'a BlockDef<'a>)>>;
 
-#[derive(Clone)]
+#[derive(Default, Clone)]
 pub(crate) struct Context<'a> {
     pub(crate) nodes: &'a [Node<'a>],
     pub(crate) extends: Option<Rc<Path>>,
diff --git a/askama_derive/src/input.rs b/askama_derive/src/input.rs
index 4139ca8..462e73b 100644
--- a/askama_derive/src/input.rs
+++ b/askama_derive/src/input.rs
@@ -332,6 +332,14 @@ impl TemplateArgs {
         Ok(args)
     }
 
+    pub(crate) fn fallback() -> Self {
+        Self {
+            source: Some(Source::Source("".to_string())),
+            ext: Some("txt".to_string()),
+            ..Self::default()
+        }
+    }
+
     pub(crate) fn config(&self) -> Result<String, CompileError> {
         read_config_file(self.config.as_deref())
     }
diff --git a/askama_derive/src/lib.rs b/askama_derive/src/lib.rs
index fd2b5c0..2f1626a 100644
--- a/askama_derive/src/lib.rs
+++ b/askama_derive/src/lib.rs
@@ -23,10 +23,26 @@ pub fn derive_template(input: TokenStream) -> TokenStream {
     let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
     match build_template(&ast) {
         Ok(source) => source.parse().unwrap(),
-        Err(e) => e.into_compile_error(),
+        Err(e) => {
+            let mut e = e.into_compile_error();
+            if let Ok(source) = build_skeleton(&ast) {
+                let source: TokenStream = source.parse().unwrap();
+                e.extend(source);
+            }
+            e
+        }
     }
 }
 
+fn build_skeleton(ast: &syn::DeriveInput) -> Result<String, CompileError> {
+    let template_args = TemplateArgs::fallback();
+    let config = Config::new("", None)?;
+    let input = TemplateInput::new(ast, &config, &template_args)?;
+    let mut contexts = HashMap::new();
+    contexts.insert(&input.path, Context::default());
+    Generator::new(&input, &contexts, None, MapChain::default()).build(&contexts[&input.path])
+}
+
 /// Takes a `syn::DeriveInput` and generates source code for it
 ///
 /// Reads the metadata from the `template()` attribute to get the template
diff --git a/book/src/filters.md b/book/src/filters.md
index 9886528..c444abb 100644
--- a/book/src/filters.md
+++ b/book/src/filters.md
@@ -26,6 +26,7 @@ Enable it with Cargo features (see below for more information).
   [`center`][#center],
   [`escape|e`][#escape],
   [`filesizeformat`][#filesizeformat],
+  [`fmt`][#fmt],
   [`format`][#format],
   [`indent`][#indent],
   [`join`][#join],
@@ -161,6 +162,28 @@ Output:
 1 KB
 ```
 
+### fmt
+
+[#fmt]: #fmt
+
+Formats arguments according to the specified format
+
+The *second* argument to this filter must be a string literal (as in normal
+Rust). The two arguments are passed through to [`format!()`] by
+the Askama code generator, but the order is swapped to support filter
+composition.
+
+```text
+{{ value|fmt("{:?}") }}
+```
+
+As an example, this allows filters to be composed like the following.
+Which is not possible using the `format` filter.
+
+```text
+{{ value|capitalize|fmt("{:?}") }}
+```
+
 ### format
 [#format]: #format
 
@@ -168,12 +191,14 @@ Formats arguments according to the specified format.
 
 The first argument to this filter must be a string literal (as in normal Rust).
 
-All arguments are passed through to the `format!()` macro by the Askama code generator.
+All arguments are passed through to [`format!()`] by the Askama code generator.
 
 ```
 {{ "{:?}"|format(var) }}
 ```
 
+[`format!()`]: https://doc.rust-lang.org/stable/std/macro.format.html
+
 ### indent
 [#indent]: #indent
 
diff --git a/book/src/template_syntax.md b/book/src/template_syntax.md
index e9d9aed..9d2cfa8 100644
--- a/book/src/template_syntax.md
+++ b/book/src/template_syntax.md
@@ -157,14 +157,14 @@ Hello
 To be noted, if one of the trimmed characters is a newline, then the only
 character remaining will be a newline.
 
-Whitespace controls can also be defined by a 
-[configuration file](configuration.md) or in the derive macro. 
+Whitespace controls can also be defined by a
+[configuration file](configuration.md) or in the derive macro.
 These definitions follow the global-to-local preference:
 1. Inline (`-`, `+`, `~`)
 2. Derive (`#[template(whitespace = "suppress")]`)
 3. Configuration (in `askama.toml`, `whitespace = "preserve"`)
 
-Two inline whitespace controls may point to the same whitespace span. 
+Two inline whitespace controls may point to the same whitespace span.
 In this case, they are resolved by the following preference.
 1. Suppress (`-`)
 2. Minimize (`~`)
@@ -172,7 +172,7 @@ In this case, they are resolved by the following preference.
 
 ## Functions
 
-There are several ways that functions can be called within templates, 
+There are several ways that functions can be called within templates,
 depending on where the function definition resides. These are:
 
 - Template `struct` fields
@@ -181,9 +181,9 @@ depending on where the function definition resides. These are:
 
 ### Template struct field
 
-When the function is a field of the template struct, we can simply call it 
+When the function is a field of the template struct, we can simply call it
 by invoking the name of the field, followed by parentheses containing any
-required arguments. For example, we can invoke the function `foo` for the 
+required arguments. For example, we can invoke the function `foo` for the
 following `MyTemplate` struct:
 
 ```rust
@@ -200,11 +200,11 @@ some behaviour for our template.
 
 ### Static functions
 
-When a function exists within the same Rust module as the template 
+When a function exists within the same Rust module as the template
 definition, we can invoke it using the `self` path prefix, where `self`
-represents the scope of the module in which the template struct resides. 
+represents the scope of the module in which the template struct resides.
 
-For example, here we call the function `foo` by writing `self::foo(123)` 
+For example, here we call the function `foo` by writing `self::foo(123)`
 within the `MyTemplate` struct source:
 
 ```rust
@@ -218,10 +218,10 @@ struct MyTemplate;
 ```
 
 This has the advantage of being able to share functionality across multiple
-templates, without needing to expose the function publicly outside of its 
+templates, without needing to expose the function publicly outside of its
 module.
 
-However, we are not limited to local functions defined within the same module. 
+However, we are not limited to local functions defined within the same module.
 We can call _any_ public function by specifying the full path to that function
 within the template source. For example, given a utilities module such as:
 
@@ -245,9 +245,9 @@ struct MyTemplate;
 
 ### Struct / trait implementations
 
-Finally, we can invoke functions that are implementation methods of our 
-template struct, by referencing `Self` (note the uppercase `S`) as the path, 
-before calling our function: 
+Finally, we can invoke functions that are implementation methods of our
+template struct, by referencing `Self` (note the uppercase `S`) as the path,
+before calling our function:
 
 ```rust
 #[derive(Template)]
@@ -263,11 +263,11 @@ impl MyTemplate {
 }
 ```
 
-If the implemented method requires a reference to the struct itself, 
-such as is demonstrated in the above example, we can pass `self` 
+If the implemented method requires a reference to the struct itself,
+such as is demonstrated in the above example, we can pass `self`
 (note the lowercase `s`) as the first argument.
 
-Similarly, using the `Self` path, we can also call any method belonging 
+Similarly, using the `Self` path, we can also call any method belonging
 to a trait that has been implemented for our template struct:
 
 ```rust
@@ -436,7 +436,8 @@ Inside for-loop blocks, some useful variables are accessible:
 
 ### If
 
-The *if* statement is used as you might expect:
+The `if` statement essentially mirrors Rust's [`if` expression],
+and is used as you might expect:
 
 ```text
 {% if users.len() == 0 %}
@@ -448,6 +449,23 @@ The *if* statement is used as you might expect:
 {% endif %}
 ```
 
+[`if` expression]: https://doc.rust-lang.org/reference/expressions/if-expr.html#if-expressions
+
+#### If Let
+
+Additionally, `if let` statements are also supported and similarly
+mirror Rust's [`if let` expressions]:
+
+```text
+{% if Some(user) = user %}
+  {{ user.name }}
+{% else %}
+  No user
+{% endif %}
+```
+
+[`if let` expressions]: https://doc.rust-lang.org/reference/expressions/if-expr.html#if-let-expressions
+
 ### Match
 
 In order to deal with Rust `enum`s in a type-safe way, templates support


Test patch:
diff --git a/testing/tests/ui/filter_block_ws.stderr b/testing/tests/ui/filter_block_ws.stderr
index 7e30f69..2740efe 100644
--- a/testing/tests/ui/filter_block_ws.stderr
+++ b/testing/tests/ui/filter_block_ws.stderr
@@ -6,16 +6,3 @@ error: failed to parse template source at row 1, column 27 near:
   |          ^^^^^^^^
   |
   = note: this error originates in the derive macro `Template` (in Nightly builds, run with -Z macro-backtrace for more info)
-
-error[E0599]: no method named `render` found for struct `A` in the current scope
-  --> tests/ui/filter_block_ws.rs:11:7
-   |
-8  | struct A;
-   | -------- method `render` not found for this struct
-...
-11 |     A.render().unwrap();
-   |       ^^^^^^ method not found in `A`
-   |
-   = help: items from traits can only be used if the trait is implemented and in scope
-   = note: the following trait defines an item `render`, perhaps you need to implement it:
-           candidate #1: `Template`