Unfortunately this doesn't seem like it can be integrated into rustc's diagnostic system, so has to use miette
errors instead. Only the "invalid reference" case is handled for now, and more will need to be added later. There's also a clear need for refactoring in fluent_embed
to make everything a bit cleaner.
F5LG7WENUUDRSCTDMA4M6BAC5RWTGQO45C4ZEBZDX6FHCTTHBVGQC
JZXXFWQKOYAFQLQZDRALXG4KGEDR7JKO3AZ5Q5X7IQTS7BCJP3QAC
RLX6XPNZKD6GIRLWKYXFH2RNIU4ZNXLMHXLOMID3E6H53QXXXNZQC
XGNME3WRU3MJDTFHUFJYARLVXWBZIH5ODBOIIFTXHNCBTZQH2R7QC
NO3PDO7PY7J3WPADNCS5VD6HKFY63E23I3SDR4DHXNVQJTG27RAAC
2XQ6ZB4WZNNR4KNC3VWNTV7IRMGGAEP33JPQUVB3CVWAKHECZVRQC
P6FW2GGOW24UZZAWQ6IDDI66JBWTIY26TATMCIOETZ4GRRGGUI3AC
4MRF5E76QSW3EPICI6TNEGJ2KSBWODWMIDQPLYALDWBYWKAV5LJAC
QSK7JRBA55ZRY322WXGNRROJL7NTFBR6MJPOOA5B2XD2JAVM4MWQC
XEEXWJLGVIPIGURSDU4ETZMGAIFTFDPECM4QWFOSRHU7GMGVOUVQC
5TEX4MNUC4LDDRMNEOVCFNUUEZAGUXMKO3OIEQFXWRQKXSHY2NRQC
HJMYJDC77NLU44QZWIW7CELXJKD4EK4YZ6CCILYBG6FWGZ2KMBVAC
D652S2N3MHR7NJWSJIT7DUH5TPEFF6YII7EGV4C7IYWARXLMGAWQC
O77KA6C4UJGZXVGPEA7WCRQH6XYQJPWETSPDXI3VOKOSRQND7JEQC
V5S5K33ALIEG5ZABUSAPO4ULHEBFDB2PLTW27A4BFS342SJG7URQC
BQ6N55O7RPG47G35YI37Z37456VKWT5KLGQKDQVAN2WI4K34TRBQC
ROSR4HD5ENPQU3HH5IVYSOA5YM72W77CHVQARSD3T67BUNYG7KZQC
3WEPY3OXJJ72TNVZLFCN2ZDWSADLT52T6DUONFGEAB46UWAQD3PQC
SHNZZSZGIBTTD4IV5SMW5BIN5DORUWQVTVTNB5RMRD5CTFNOMJ6AC
VYPJUPPKPVSIDCQPKZE2RJLMUQDI2IYV5COBQAEXI3VF3SF4CQTAC
UKFEFT6LSI4K7X6UHQFZYD52DILKXMZMYSO2UYS2FCHNPXIF4BEQC
VNSHGQYNPGKGGPYNVP4Z2RWD7JCSDJVYAADD6UXWBYL6ZRXKLE4AC
WBI5HFOBBUMDSGKY2RX3YA6N7YDCJEP23JNEJ7PG5VZXHLYIRJRQC
56F2YE6HUZ76U4QBPUDJ2VQLJ75TQYNTVQIOX4QBOZ2H6GJKRGUQC
VZYZRAO4EXCHW2LBVFG5ELSWG5SCNDREMJ6RKQ4EKQGI2T7SD3ZQC
}
})
}
/// Create a list of unique field names that can be referenced
fn unique_named_fields(named_fields: &syn::FieldsNamed) -> HashSet<String> {
named_fields
.named
.iter()
// Get the `syn::Ident` for each field
.map(|field| {
field
.ident
.as_ref()
.expect("Named fields should have an associated ident")
})
.map(|ident| ident.to_string())
.collect::<HashSet<String>>()
// TODO: check that the fields in fluent source reference fields that exist
pub fn derive_struct(group: Group, ident: &syn::Ident) -> TokenStream {
pub fn derive_struct(
group: Group,
ident: &syn::Ident,
fields: &syn::Fields,
) -> Result<TokenStream, FluentError> {
// Turn the struct fields into a list of valid references
let references = match fields {
syn::Fields::Named(named_fields) => References {
// Reference using `self.{reference_name}`
kind: ReferenceKind::StructField,
// Create a list of unique field names that can be referenced
candidates: unique_named_fields(named_fields),
},
syn::Fields::Unnamed(_) => todo!(),
syn::Fields::Unit => todo!(),
};
let arm_body = expr_for_message(&group, &variant_kebab_case, ReferenceKind::EnumField);
let references = match &enum_variant.fields {
syn::Fields::Named(named_fields) => References {
kind: ReferenceKind::EnumField,
candidates: unique_named_fields(named_fields),
},
syn::Fields::Unnamed(_) => todo!(),
syn::Fields::Unit => todo!(),
};
let arm_body = expr_for_message(&group, &variant_kebab_case, &references)?;
pub(crate) fn pattern(pattern: &Pattern<String>, reference_kind: ReferenceKind) -> syn::Expr {
#[derive(Clone, Debug)]
pub struct References {
pub kind: ReferenceKind,
pub candidates: HashSet<String>,
}
pub(crate) fn pattern(
message: &Message<String>,
pattern: &Pattern<String>,
references: &References,
path: &str,
) -> Result<syn::Expr, FluentError> {
let expression = placeable_expression(&expression, reference_kind);
let expression = match expression {
Expression::Select { selector, variants } => {
let target = inline_expression(message, selector, references, path)?;
let arms: Vec<syn::Arm> = variants
.iter()
.map(|item| variant(message, item, references, path))
.collect::<Result<_, FluentError>>()?;
parse_quote! {
match plural_rules.category_for(#target) {
#(#arms),*
}
}
}
Expression::Inline(expression) => {
inline_expression(message, expression, references, path)?
}
};
fn placeable_expression(expression: &Expression<String>, reference_kind: ReferenceKind) -> syn::Expr {
match expression {
Expression::Select { selector, variants } => {
let target = inline_expression(selector, reference_kind);
let arms: Vec<syn::Arm> = variants.iter().map(|item| variant(item, reference_kind)).collect();
parse_quote! {
match plural_rules.category_for(#target) {
#(#arms),*
}
}
}
Expression::Inline(expression) => inline_expression(expression, reference_kind),
}
Ok(parse_quote!(
format!(#format_body_literal, #(#format_arguments),*)
))
fn inline_expression(expression: &InlineExpression<String>, reference_kind: ReferenceKind) -> syn::Expr {
match expression {
fn inline_expression(
message: &Message<String>,
expression: &InlineExpression<String>,
references: &References,
path: &str,
) -> Result<syn::Expr, FluentError> {
Ok(match expression {
let ident = format_ident!("{}", id.name.to_snake_case());
match reference_kind {
// Make sure the referenced variable is in the set of valid variables
// let ident = format_ident!("{}", id.name.to_snake_case());
let ident = if let Some(variable) = references.candidates.get(&id.name.to_snake_case())
{
format_ident!("{variable}")
} else {
// Create a fake `fluent_syntax::ast::Resource` to serialize into a String
let error_resource = Resource {
body: vec![Entry::Message(message.to_owned())],
};
let source_string = fluent_syntax::serializer::serialize_with_options(
&error_resource,
fluent_syntax::serializer::Options {
// Make sure to include all source code in error snippet, even if marked as "junk"
with_junk: true,
},
);
let location = source_string.find(&format!("${}", id.name)).unwrap();
return Err(FluentError::InvalidReference(InvalidReference {
src: NamedSource::new(path, source_string.clone()),
span: (location..location + id.name.len()).into(),
help: format!(
"the following references are valid:\n{}",
references
.candidates
.iter()
.map(|field| format!("- ${}", field.to_lower_camel_case()))
.collect::<Vec<String>>()
.join("\n")
),
}));
};
match references.kind {
}
fn message_column(&self, id: &str) -> usize {
self.canonical_messages
.iter()
.position(|message| message.id.name == id)
.expect("Message id must be valid")
pub fn canonical_message(&self, id: &str, reference_kind: ReferenceKind) -> syn::Expr {
let message_column = self.message_column(id);
let message = &self.canonical_messages[message_column];
parse_fluent::message(message, reference_kind)
pub fn canonical_message(
&self,
id: &str,
references: &References,
) -> Result<syn::Expr, FluentError> {
let message = self
.canonical_messages
.iter()
.find(|message| message.id.name == id)
.expect("Message id must be valid");
let path = self.paths.get(&self.canonical_locale).unwrap();
parse_fluent::message(message, references, path)
reference_kind: ReferenceKind,
) -> impl Iterator<Item = (&Locale, syn::Expr)> {
let message_column = self.message_column(id);
references: &References,
) -> Result<Vec<(&Locale, syn::Expr)>, FluentError> {
let mut messages = Vec::with_capacity(self.extra_locales.len());
let message_column = self
.canonical_messages
.iter()
.position(|message| message.id.name == id)
.expect("Message id must be valid");
self.extra_locales.iter().filter_map(move |locale_group| {
locale_group
.messages
.get(message_column)
.unwrap()
.as_ref()
.map(|message: &Message<String>| {
(
&locale_group.locale,
parse_fluent::message(message, reference_kind),
)
})
})
for locale_group in &self.extra_locales {
// Include the message only if it exists in this locale
if let Some(message) = &locale_group.messages[message_column] {
let path = self.paths.get(&locale_group.locale).unwrap();
let message_expr = parse_fluent::message(&message, references, path)?;
messages.push((&locale_group.locale, message_expr))
}
}
Ok(messages)
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";
};
}
}
}
use fluent_embed::AttributeError;
use proc_macro::TokenStream;
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();
match error {
FluentError::InvalidReference(invalid_reference) => {
eprintln!("{:?}", miette::Error::new(invalid_reference));
}
}
// Make sure compilation fails
emit_call_site_error!("invalid Fluent source code, see above for details");
}
match attribute_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");
};
match macro_error {
MacroError::Attribute(error) => attribute_error(error, &derive_attribute),
MacroError::Fluent(error) => fluent_error(error),
}
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";
};
}
};
[[package]]
name = "backtrace"
version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [
"addr2line",
"cc",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "backtrace-ext"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
dependencies = [
"backtrace",
]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
name = "miette"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
dependencies = [
"backtrace",
"backtrace-ext",
"cfg-if",
"miette-derive",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size",
"textwrap",
"thiserror",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]]
name = "supports-color"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f"
dependencies = [
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee"
[[package]]
name = "supports-unicode"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2"
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "textwrap"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]