Part of an extended cleanup effort, fluent_embed
will likely get renamed to fluent_embed_derive
and fluent_embed_runtime
will take its place, re-exporting the macro similar to serde
.
OWXLFLRMQDTXWN5QQQLJNAATWFWXIN2S4UQA2LC2A6AWX4UWM6LQC
BANMRGROVYKYRJ4N2P4HSOJ2JVV6VSEB3W34BFXPOEFND5O36CGAC
2SITVDYW6KANM24QXRHVSBL6S77UHKJLOSOHSUZQBJFL5NAAGQYAC
AL3CCMWZO6HUEGGNHILIQE4V3W2Z3CUOSNUTDPETCSN5IWWVITDQC
KDUI7LHJRRQRFYPY7ANUNXG6XCUKQ4YYOEL5NG5Y6BRMV6GQ5M7AC
SHNZZSZGIBTTD4IV5SMW5BIN5DORUWQVTVTNB5RMRD5CTFNOMJ6AC
F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC
V5S5K33ALIEG5ZABUSAPO4ULHEBFDB2PLTW27A4BFS342SJG7URQC
NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC
HHJDRLLNN36UNIA7STAXEEVBCEMPJNB7SJQOS3TJLLYN4AEZ4MHQC
RLX6XPNZKD6GIRLWKYXFH2RNIU4ZNXLMHXLOMID3E6H53QXXXNZQC
VYPJUPPKPVSIDCQPKZE2RJLMUQDI2IYV5COBQAEXI3VF3SF4CQTAC
XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC
O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC
QFPQZR4K4UZ7R2GQZJG4NYBGVQJVL2ANIKGGTOHAMIRIBQHPSQGAC
XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC
UOMQT7LTURIIWHZT2ZHLCJG6XESYTN26EJC7IHRFR4PYJ355PNYAC
BFL2Y7GN6NBXXNAUSD4M6T6CIVQ2OLERPE2CAFSLRF377WFFTVCQC
ROSR4HD5ENPQU3HH5IVYSOA5YM72W77CHVQARSD3T67BUNYG7KZQC
56F2YE6HUZ76U4QBPUDJ2VQLJ75TQYNTVQIOX4QBOZ2H6GJKRGUQC
VZYZRAO4EXCHW2LBVFG5ELSWG5SCNDREMJ6RKQ4EKQGI2T7SD3ZQC
JZXXFWQKOYAFQLQZDRALXG4KGEDR7JKO3AZ5Q5X7IQTS7BCJP3QAC
VNSHGQYNPGKGGPYNVP4Z2RWD7JCSDJVYAADD6UXWBYL6ZRXKLE4AC
UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC
WBI5HFOBBUMDSGKY2RX3YA6N7YDCJEP23JNEJ7PG5VZXHLYIRJRQC
[package]
name = "cli_macros"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[lints]
workspace = true
[dependencies]
fluent_embed = { path = "../fluent_embed" }
miette = { version = "7.2.0", features = ["fancy"] }
proc-macro2 = "1.0.78"
quote = "1.0.35"
proc-macro-error = "1.0.4"
syn = { version = "2.0.48", features = ["full", "extra-traits"] }
wax = { git = "https://github.com/Finchiedev/wax" }
[dev-dependencies]
fluent_embed_runtime = { path = "../fluent_embed_runtime" }
//! A proc_macro shim for other crates in this workspace
use fluent_embed::{AttributeError, FluentError, MacroError};
use proc_macro::TokenStream;
use proc_macro_error::{abort, emit_call_site_error, emit_error, proc_macro_error};
use quote::{quote, ToTokens};
use syn::parse_macro_input;
fn attribute_error(error: AttributeError, derive_attribute: &syn::LitStr) {
match error {
AttributeError::Build(build_error) => {
for location in build_error.locations() {
// Create a token stream from the attribute's string literal
let [proc_macro2::TokenTree::Literal(ref string_literal)] = derive_attribute
.to_token_stream()
.into_iter()
.collect::<Vec<_>>()[..]
else {
abort!(derive_attribute, "unexpected macro attribute");
};
let (span_start, span_length) = location.span();
let error_source = string_literal
// Offset by 1 to skip the starting `"` double-quote character
.subspan(span_start + 1..=span_start + span_length)
// Fall back to the whole attribute if `subspan()` returns `None`
// This will always happend on stable as subspan is nightly-only:
// https://docs.rs/proc-macro2/latest/proc_macro2/struct.Literal.html#method.subspan
.unwrap_or(derive_attribute.span());
emit_error! { error_source, "invalid glob";
note = location.to_string();
};
}
}
AttributeError::Walk(walk_error) => {
// Generate help text
let help = if let Some(path) = walk_error.path() {
let path_name = path.to_str().unwrap();
// Might hit an error if file exists but insufficient permissions
match path.try_exists() {
Ok(true) => {
format!("the path `{path_name}` exists, but unable to access it")
}
_ => format!("the path `{path_name}` doesn't seem to exist"),
}
} else {
String::from("no associated path")
};
emit_error! { derive_attribute, "error at depth {} while walking path", walk_error.depth();
help = help;
};
}
AttributeError::NoMatches {
path,
complete_match,
} => {
// Validate the assumption that the user has provided an exact path
assert!(path.exists());
assert_eq!(path.to_string_lossy(), complete_match);
emit_error! { derive_attribute, "cannot match against an exact path";
help = "The attribute should use glob syntax to match against multiple files";
note = "For example, you can:\n{}\n{}",
"- Match against directories: locale/**/errors.ftl",
"- Match against files: locale/*.ftl";
};
}
}
}
fn fluent_error(error: FluentError) {
// It doesn't seem like you can reference non-Rust source files
// in `proc_macro::Span`, so use Miette to pretty-print our own reports.
// This includes setting up a global report handler with some extra options
miette::set_hook(Box::new(|_| {
Box::new(
miette::MietteHandlerOpts::new()
// Force color output, even when printing using the debug formatter
.color(true)
.build(),
)
}))
.unwrap();
eprintln!("{:?}", miette::Error::new(error));
// Make sure compilation fails
emit_call_site_error!("invalid Fluent source code, see above for details");
}
#[proc_macro_attribute]
#[proc_macro_error]
pub fn localize(attribute: TokenStream, item: TokenStream) -> TokenStream {
let original_item = proc_macro2::TokenStream::from(item.clone());
let derive_attribute: syn::LitStr = parse_macro_input!(attribute);
let derive_input = parse_macro_input!(item);
let implementation = match fluent_embed::localize(&derive_attribute, &derive_input) {
Ok(implementation) => implementation,
Err(macro_error) => {
// Emit the relevant error messages
match macro_error {
MacroError::Attribute(error) => attribute_error(error, &derive_attribute),
MacroError::Fluent(error) => fluent_error(error),
}
// Generate a minimal `localize()` implementation so the error is self-contained
let ident = &derive_input.ident;
quote! {
impl #ident {
fn localize(&self) -> String {
unimplemented!("Encountered error in proc-macro")
}
}
}
}
};
quote! {
#original_item
#implementation
}
.into()
}
#[proc_macro_attribute]
#[proc_macro_error]
pub fn localize(attribute: TokenStream, item: TokenStream) -> TokenStream {
let original_item = proc_macro2::TokenStream::from(item.clone());
let derive_attribute: syn::LitStr = parse_macro_input!(attribute);
let derive_input = parse_macro_input!(item);
let implementation = localize_impl(&derive_attribute, &derive_input)
.unwrap_or_else(|error| error::handle(error, &derive_attribute, &derive_input.ident));
quote! {
#original_item
#implementation
}
.into()
}
use super::{AttributeError, MacroError};
use proc_macro_error::{abort, emit_call_site_error, emit_error};
use quote::{quote, ToTokens};
fn attribute(error: AttributeError, derive_attribute: &syn::LitStr) {
match error {
AttributeError::Build(build_error) => {
for location in build_error.locations() {
// Create a token stream from the attribute's string literal
let [proc_macro2::TokenTree::Literal(ref string_literal)] = derive_attribute
.to_token_stream()
.into_iter()
.collect::<Vec<_>>()[..]
else {
abort!(derive_attribute, "unexpected macro attribute");
};
let (span_start, span_length) = location.span();
let error_source = string_literal
// Offset by 1 to skip the starting `"` double-quote character
.subspan(span_start + 1..=span_start + span_length)
// Fall back to the whole attribute if `subspan()` returns `None`
// This will always happend on stable as subspan is nightly-only:
// https://docs.rs/proc-macro2/latest/proc_macro2/struct.Literal.html#method.subspan
.unwrap_or(derive_attribute.span());
emit_error! { error_source, "invalid glob";
note = location.to_string();
};
}
}
AttributeError::Walk(walk_error) => {
// Generate help text
let help = if let Some(path) = walk_error.path() {
let path_name = path.to_str().unwrap();
// Might hit an error if file exists but insufficient permissions
match path.try_exists() {
Ok(true) => {
format!("the path `{path_name}` exists, but unable to access it")
}
_ => format!("the path `{path_name}` doesn't seem to exist"),
}
} else {
String::from("no associated path")
};
emit_error! { derive_attribute, "error at depth {} while walking path", walk_error.depth();
help = help;
};
}
AttributeError::NoMatches {
path,
complete_match,
} => {
// Validate the assumption that the user has provided an exact path
assert!(path.exists());
assert_eq!(path.to_string_lossy(), complete_match);
emit_error! { derive_attribute, "cannot match against an exact path";
help = "The attribute should use glob syntax to match against multiple files";
note = "For example, you can:\n{}\n{}",
"- Match against directories: locale/**/errors.ftl",
"- Match against files: locale/*.ftl";
};
}
}
}
fn fluent(error: super::fluent::Error) {
// It doesn't seem like you can reference non-Rust source files
// in `proc_macro::Span`, so use Miette to pretty-print our own reports.
// This includes setting up a global report handler with some extra options
miette::set_hook(Box::new(|_| {
Box::new(
miette::MietteHandlerOpts::new()
// Force color output, even when printing using the debug formatter
.color(true)
.build(),
)
}))
.unwrap();
eprintln!("{:?}", miette::Error::new(error));
// Make sure compilation fails
emit_call_site_error!("invalid Fluent source code, see above for details");
}
pub fn handle(
error: MacroError,
derive_attribute: &syn::LitStr,
ident: &syn::Ident,
) -> proc_macro2::TokenStream {
match error {
MacroError::Attribute(error) => attribute(error, derive_attribute),
MacroError::Fluent(error) => fluent(error),
};
// Generate a minimal `localize()` implementation so the error is self-contained
quote! {
impl #ident {
fn localize(&self) -> String {
unimplemented!("Encountered error in derive macro")
}
}
}
}
[[package]]
name = "cli_macros"
version = "0.1.0"
dependencies = [
"fluent_embed",
"fluent_embed_runtime",
"miette",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.71",
"wax",
]
name = "prettyplease"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
dependencies = [
"proc-macro2",
"syn 2.0.71",
]
[[package]]