Merge pull request #87 from Kijewski/pr-include-once

Every used template gets referenced exactly once
This commit is contained in:
Guillaume Gomez 2024-07-23 13:43:13 +02:00 committed by GitHub
commit 7eddf9d2ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 138 additions and 64 deletions

View File

@ -33,4 +33,10 @@ once_map = "0.4.18"
proc-macro2 = "1"
quote = "1"
serde = { version = "1.0", optional = true, features = ["derive"] }
syn = "2"
syn = "2.0.3"
[dev-dependencies]
console = "0.15.8"
similar = "2.6.0"
prettyplease = "0.2.20"
syn = { version = "2.0.3", features = ["extra-traits", "full"] }

View File

@ -102,20 +102,23 @@ impl<'a> Generator<'a> {
buf.discard = self.buf_writable.discard;
// Make sure the compiler understands that the generated code depends on the template files.
for path in self.contexts.keys() {
let mut paths = self
.contexts
.keys()
.map(|path| -> &Path { path })
.collect::<Vec<_>>();
paths.sort();
for path in paths {
// Skip the fake path of templates defined in rust source.
let path_is_valid = match self.input.source {
Source::Path(_) => true,
Source::Source(_) => **path != self.input.path,
Source::Source(_) => path != &*self.input.path,
};
if path_is_valid {
let path = path.to_str().unwrap();
buf.writeln(
quote! {
const _: &[::core::primitive::u8] = ::core::include_bytes!(#path);
}
.to_string(),
);
buf.writeln(format_args!(
"const _: &[::core::primitive::u8] = ::core::include_bytes!({path:#?});",
));
}
}
@ -788,17 +791,6 @@ impl<'a> Generator<'a> {
.config
.find_template(i.path, Some(&self.input.path))?;
// Make sure the compiler understands that the generated code depends on the template file.
{
let path = path.to_str().unwrap();
buf.writeln(
quote! {
const _: &[::core::primitive::u8] = ::core::include_bytes!(#path);
}
.to_string(),
);
}
// We clone the context of the child in order to preserve their macros and imports.
// But also add all the imports and macros from this template that don't override the
// child's ones to preserve this template's context.

View File

@ -1,6 +1,10 @@
//! Files containing tests for generated code.
use std::fmt::Write;
use std::fmt;
use std::path::Path;
use console::style;
use similar::{Algorithm, ChangeTag, TextDiffConfig};
use crate::build_template;
@ -9,51 +13,91 @@ fn check_if_let() {
// This function makes it much easier to compare expected code by adding the wrapping around
// the code we want to check.
#[track_caller]
fn compare(jinja: &str, expected: &str) {
let jinja = format!(
r##"#[template(source = r#"{jinja}"#, ext = "txt")]
struct Foo;"##
);
let generated =
build_template(&syn::parse_str::<syn::DeriveInput>(&jinja).unwrap()).unwrap();
fn compare(jinja: &str, expected: &str, size_hint: usize) {
let jinja = format!(r#"#[template(source = {jinja:?}, ext = "txt")] struct Foo;"#);
let generated = build_template(&syn::parse_str::<syn::DeriveInput>(&jinja).unwrap())
.unwrap()
.parse()
.unwrap();
let generated: syn::File = syn::parse2(generated).unwrap();
let generated_s = syn::parse_str::<proc_macro2::TokenStream>(&generated)
.unwrap()
.to_string();
let mut new_expected = String::with_capacity(expected.len());
for line in expected.split('\n') {
new_expected.write_fmt(format_args!("{line}\n")).unwrap();
let size_hint = proc_macro2::Literal::usize_unsuffixed(size_hint);
let expected: proc_macro2::TokenStream = expected.parse().unwrap();
let expected: syn::File = syn::parse_quote! {
impl ::rinja::Template for Foo {
fn render_into<RinjaW>(&self, writer: &mut RinjaW) -> ::rinja::Result<()>
where
RinjaW: ::core::fmt::Write + ?::core::marker::Sized,
{
use ::rinja::filters::AutoEscape as _;
use ::core::fmt::Write as _;
#expected
::rinja::Result::Ok(())
}
const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = Some("txt");
const SIZE_HINT: ::std::primitive::usize = #size_hint;
const MIME_TYPE: &'static ::std::primitive::str = "text/plain; charset=utf-8";
}
impl ::std::fmt::Display for Foo {
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
::rinja::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {})
}
}
};
if expected != generated {
let expected = prettyplease::unparse(&expected);
let generated = prettyplease::unparse(&generated);
struct Diff<'a>(&'a str, &'a str);
impl fmt::Display for Diff<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let diff = TextDiffConfig::default()
.algorithm(Algorithm::Patience)
.diff_lines(self.0, self.1);
for change in diff.iter_all_changes() {
let (change, line) = match change.tag() {
ChangeTag::Equal => (
style(" ").dim().bold(),
style(change.to_string_lossy()).dim(),
),
ChangeTag::Delete => (
style("-").red().bold(),
style(change.to_string_lossy()).red(),
),
ChangeTag::Insert => (
style("+").green().bold(),
style(change.to_string_lossy()).green(),
),
};
write!(f, "{change}{line}")?;
}
Ok(())
}
}
panic!(
"\n\
=== Expected ===\n\
\n\
{expected}\n\
\n\
=== Generated ===\n\
\n\
{generated}\n\
\n\
=== Diff ===\n\
\n\
{diff}\n\
\n\
=== FAILURE ===",
expected = style(&expected).red(),
generated = style(&generated).green(),
diff = Diff(&expected, &generated),
);
}
let expected = format!(
r#"impl ::rinja::Template for Foo {{
fn render_into<RinjaW>(&self, writer: &mut RinjaW) -> ::rinja::Result<()>
where
RinjaW: ::core::fmt::Write + ?::core::marker::Sized,
{{
use ::rinja::filters::AutoEscape as _;
use ::core::fmt::Write as _;
{new_expected}
::rinja::Result::Ok(())
}}
const EXTENSION: ::std::option::Option<&'static ::std::primitive::str> = Some("txt");
const SIZE_HINT: ::std::primitive::usize = 3;
const MIME_TYPE: &'static ::std::primitive::str = "text/plain; charset=utf-8";
}}
impl ::std::fmt::Display for Foo {{
#[inline]
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {{
::rinja::Template::render_into(self, f).map_err(|_| ::std::fmt::Error {{}})
}}
}}"#
);
let expected_s = syn::parse_str::<proc_macro2::TokenStream>(&expected)
.unwrap()
.to_string();
assert_eq!(
generated_s, expected_s,
"=== Expected ===\n{}\n=== Found ===\n{}\n=====",
generated, expected
);
}
// In this test, we ensure that `query` never is `self.query`.
@ -66,6 +110,7 @@ impl ::std::fmt::Display for Foo {{
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(query), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
3,
);
// In this test, we ensure that `s` is `self.s` only in the first `if let Some(s) = self.s`
@ -79,6 +124,7 @@ impl ::std::fmt::Display for Foo {{
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
3,
);
// In this test, we ensure that `s` is `self.s` only in the first `if let Some(s) = self.s`
@ -92,5 +138,25 @@ impl ::std::fmt::Display for Foo {{
expr0 = &(&&::rinja::filters::AutoEscaper::new(&(s), ::rinja::filters::Text)).rinja_auto_escape()?,
)?;
}"#,
3,
);
// In this test we make sure that every used template gets referenced exactly once.
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("templates");
let path1 = path.join("include1.html");
let path2 = path.join("include2.html");
let path3 = path.join("include3.html");
compare(
r#"{% include "include1.html" %}"#,
&format!(
r#"const _: &[::core::primitive::u8] = ::core::include_bytes!({path1:#?});
const _: &[::core::primitive::u8] = ::core::include_bytes!({path2:#?});
const _: &[::core::primitive::u8] = ::core::include_bytes!({path3:#?});
writer.write_str("3")?;
writer.write_str("3")?;
writer.write_str("3")?;
writer.write_str("3")?;"#
),
4,
);
}

View File

@ -0,0 +1,2 @@
{%- include "include2.html" -%}
{%- include "include2.html" -%}

View File

@ -0,0 +1,2 @@
{%- include "include3.html" -%}
{%- include "include3.html" -%}

View File

@ -0,0 +1 @@
3

View File

@ -37,6 +37,11 @@ syn = "2"
[dev-dependencies]
criterion = "0.5"
console = "0.15.8"
similar = "2.6.0"
prettyplease = "0.2.20"
syn = { version = "2.0.3", features = ["extra-traits", "full"] }
[[bench]]
name = "derive-template"
harness = false