2022-05-23 18:24:55 +01:00
|
|
|
use annotate_snippets::{
|
|
|
|
display_list::DisplayList,
|
|
|
|
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
|
|
|
|
};
|
|
|
|
use fluent_bundle::{FluentBundle, FluentError, FluentResource};
|
|
|
|
use fluent_syntax::{
|
|
|
|
ast::{Attribute, Entry, Identifier, Message},
|
|
|
|
parser::ParserError,
|
|
|
|
};
|
|
|
|
use proc_macro::{Diagnostic, Level, Span};
|
|
|
|
use proc_macro2::TokenStream;
|
|
|
|
use quote::quote;
|
|
|
|
use std::{
|
2022-05-24 15:09:47 +01:00
|
|
|
collections::{HashMap, HashSet},
|
2022-05-23 18:24:55 +01:00
|
|
|
fs::File,
|
|
|
|
io::Read,
|
|
|
|
path::{Path, PathBuf},
|
|
|
|
};
|
|
|
|
use syn::{
|
|
|
|
parse::{Parse, ParseStream},
|
|
|
|
parse_macro_input,
|
|
|
|
punctuated::Punctuated,
|
|
|
|
token, Ident, LitStr, Result,
|
|
|
|
};
|
|
|
|
use unic_langid::langid;
|
|
|
|
|
|
|
|
struct Resource {
|
2022-10-21 12:25:25 +02:00
|
|
|
krate: Ident,
|
2022-05-23 18:24:55 +01:00
|
|
|
#[allow(dead_code)]
|
|
|
|
fat_arrow_token: token::FatArrow,
|
2022-10-21 12:25:25 +02:00
|
|
|
resource_path: LitStr,
|
2022-05-23 18:24:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Parse for Resource {
|
|
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
|
|
Ok(Resource {
|
2022-10-21 12:25:25 +02:00
|
|
|
krate: input.parse()?,
|
2022-05-23 18:24:55 +01:00
|
|
|
fat_arrow_token: input.parse()?,
|
2022-10-21 12:25:25 +02:00
|
|
|
resource_path: input.parse()?,
|
2022-05-23 18:24:55 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Resources(Punctuated<Resource, token::Comma>);
|
|
|
|
|
|
|
|
impl Parse for Resources {
|
|
|
|
fn parse(input: ParseStream<'_>) -> Result<Self> {
|
|
|
|
let mut resources = Punctuated::new();
|
|
|
|
loop {
|
|
|
|
if input.is_empty() || input.peek(token::Brace) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let value = input.parse()?;
|
|
|
|
resources.push_value(value);
|
|
|
|
if !input.peek(token::Comma) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
let punct = input.parse()?;
|
|
|
|
resources.push_punct(punct);
|
|
|
|
}
|
|
|
|
Ok(Resources(resources))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Helper function for returning an absolute path for macro-invocation relative file paths.
|
|
|
|
///
|
|
|
|
/// If the input is already absolute, then the input is returned. If the input is not absolute,
|
|
|
|
/// then it is appended to the directory containing the source file with this macro invocation.
|
|
|
|
fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
|
|
|
|
let path = Path::new(path);
|
|
|
|
if path.is_absolute() {
|
|
|
|
path.to_path_buf()
|
|
|
|
} else {
|
|
|
|
// `/a/b/c/foo/bar.rs` contains the current macro invocation
|
|
|
|
let mut source_file_path = span.source_file().path();
|
|
|
|
// `/a/b/c/foo/`
|
|
|
|
source_file_path.pop();
|
|
|
|
// `/a/b/c/foo/../locales/en-US/example.ftl`
|
|
|
|
source_file_path.push(path);
|
|
|
|
source_file_path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// See [rustc_macros::fluent_messages].
|
|
|
|
pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|
|
|
let resources = parse_macro_input!(input as Resources);
|
|
|
|
|
|
|
|
// Cannot iterate over individual messages in a bundle, so do that using the
|
|
|
|
// `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
|
|
|
|
// messages in the resources.
|
|
|
|
let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
|
|
|
|
|
|
|
|
// Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
|
|
|
|
// diagnostics.
|
|
|
|
let mut previous_defns = HashMap::new();
|
|
|
|
|
2022-10-21 12:25:25 +02:00
|
|
|
// Set of Fluent attribute names already output, to avoid duplicate type errors - any given
|
|
|
|
// constant created for a given attribute is the same.
|
|
|
|
let mut previous_attrs = HashSet::new();
|
|
|
|
|
2022-05-23 18:24:55 +01:00
|
|
|
let mut includes = TokenStream::new();
|
|
|
|
let mut generated = TokenStream::new();
|
|
|
|
|
2022-10-21 12:25:25 +02:00
|
|
|
for res in resources.0 {
|
|
|
|
let krate_span = res.krate.span().unwrap();
|
|
|
|
let path_span = res.resource_path.span().unwrap();
|
2022-05-24 15:09:47 +01:00
|
|
|
|
2022-10-21 12:25:25 +02:00
|
|
|
let relative_ftl_path = res.resource_path.value();
|
2022-05-23 18:24:55 +01:00
|
|
|
let absolute_ftl_path =
|
2022-10-21 12:25:25 +02:00
|
|
|
invocation_relative_path_to_absolute(krate_span, &relative_ftl_path);
|
2022-05-23 18:24:55 +01:00
|
|
|
// As this macro also outputs an `include_str!` for this file, the macro will always be
|
|
|
|
// re-executed when the file changes.
|
|
|
|
let mut resource_file = match File::open(absolute_ftl_path) {
|
|
|
|
Ok(resource_file) => resource_file,
|
|
|
|
Err(e) => {
|
|
|
|
Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
|
|
|
|
.note(e.to_string())
|
|
|
|
.emit();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let mut resource_contents = String::new();
|
|
|
|
if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
|
|
|
|
Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
|
|
|
|
.note(e.to_string())
|
|
|
|
.emit();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let resource = match FluentResource::try_new(resource_contents) {
|
|
|
|
Ok(resource) => resource,
|
|
|
|
Err((this, errs)) => {
|
|
|
|
Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
|
|
|
|
.help("see additional errors emitted")
|
|
|
|
.emit();
|
|
|
|
for ParserError { pos, slice: _, kind } in errs {
|
|
|
|
let mut err = kind.to_string();
|
|
|
|
// Entirely unnecessary string modification so that the error message starts
|
|
|
|
// with a lowercase as rustc errors do.
|
|
|
|
err.replace_range(
|
|
|
|
0..1,
|
|
|
|
&err.chars().next().unwrap().to_lowercase().to_string(),
|
|
|
|
);
|
|
|
|
|
|
|
|
let line_starts: Vec<usize> = std::iter::once(0)
|
|
|
|
.chain(
|
|
|
|
this.source()
|
|
|
|
.char_indices()
|
|
|
|
.filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
|
|
|
|
)
|
|
|
|
.collect();
|
|
|
|
let line_start = line_starts
|
|
|
|
.iter()
|
|
|
|
.enumerate()
|
|
|
|
.map(|(line, idx)| (line + 1, idx))
|
|
|
|
.filter(|(_, idx)| **idx <= pos.start)
|
|
|
|
.last()
|
|
|
|
.unwrap()
|
|
|
|
.0;
|
|
|
|
|
|
|
|
let snippet = Snippet {
|
|
|
|
title: Some(Annotation {
|
|
|
|
label: Some(&err),
|
|
|
|
id: None,
|
|
|
|
annotation_type: AnnotationType::Error,
|
|
|
|
}),
|
|
|
|
footer: vec![],
|
|
|
|
slices: vec![Slice {
|
|
|
|
source: this.source(),
|
|
|
|
line_start,
|
|
|
|
origin: Some(&relative_ftl_path),
|
|
|
|
fold: true,
|
|
|
|
annotations: vec![SourceAnnotation {
|
|
|
|
label: "",
|
|
|
|
annotation_type: AnnotationType::Error,
|
|
|
|
range: (pos.start, pos.end - 1),
|
|
|
|
}],
|
|
|
|
}],
|
|
|
|
opt: Default::default(),
|
|
|
|
};
|
|
|
|
let dl = DisplayList::from(snippet);
|
|
|
|
eprintln!("{}\n", dl);
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut constants = TokenStream::new();
|
|
|
|
for entry in resource.entries() {
|
2022-10-21 12:25:25 +02:00
|
|
|
let span = res.krate.span();
|
2022-05-23 18:24:55 +01:00
|
|
|
if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry {
|
2022-08-17 12:12:59 +02:00
|
|
|
let _ = previous_defns.entry(name.to_string()).or_insert(path_span);
|
2022-05-23 18:24:55 +01:00
|
|
|
|
2022-08-10 11:31:31 +02:00
|
|
|
if name.contains('-') {
|
|
|
|
Diagnostic::spanned(
|
2022-08-17 12:12:59 +02:00
|
|
|
path_span,
|
2022-08-10 11:31:31 +02:00
|
|
|
Level::Error,
|
|
|
|
format!("name `{name}` contains a '-' character"),
|
|
|
|
)
|
|
|
|
.help("replace any '-'s with '_'s")
|
|
|
|
.emit();
|
|
|
|
}
|
|
|
|
|
2022-10-21 12:25:25 +02:00
|
|
|
// Require that the message name starts with the crate name
|
|
|
|
// `hir_typeck_foo_bar` (in `hir_typeck.ftl`)
|
|
|
|
// `const_eval_baz` (in `const_eval.ftl`)
|
2022-08-10 11:31:31 +02:00
|
|
|
// `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`)
|
|
|
|
// The last case we error about above, but we want to fall back gracefully
|
|
|
|
// so that only the error is being emitted and not also one about the macro
|
|
|
|
// failing.
|
2022-10-21 12:25:25 +02:00
|
|
|
let crate_prefix = format!("{}_", res.krate);
|
2022-08-17 12:09:06 +02:00
|
|
|
|
|
|
|
let snake_name = name.replace('-', "_");
|
2022-10-21 12:25:25 +02:00
|
|
|
if !snake_name.starts_with(&crate_prefix) {
|
|
|
|
Diagnostic::spanned(
|
|
|
|
path_span,
|
|
|
|
Level::Error,
|
|
|
|
format!("name `{name}` does not start with the crate name"),
|
|
|
|
)
|
|
|
|
.help(format!(
|
|
|
|
"prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
|
|
|
|
))
|
|
|
|
.emit();
|
2022-08-17 12:09:06 +02:00
|
|
|
};
|
|
|
|
|
2022-10-21 12:25:25 +02:00
|
|
|
let snake_name = Ident::new(&snake_name, span);
|
|
|
|
|
2022-05-23 18:24:55 +01:00
|
|
|
constants.extend(quote! {
|
|
|
|
pub const #snake_name: crate::DiagnosticMessage =
|
|
|
|
crate::DiagnosticMessage::FluentIdentifier(
|
|
|
|
std::borrow::Cow::Borrowed(#name),
|
|
|
|
None
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
|
2022-07-20 11:48:11 +02:00
|
|
|
let snake_name = Ident::new(&attr_name.replace('-', "_"), span);
|
2022-05-24 15:09:47 +01:00
|
|
|
if !previous_attrs.insert(snake_name.clone()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-08-10 11:31:31 +02:00
|
|
|
if attr_name.contains('-') {
|
|
|
|
Diagnostic::spanned(
|
2022-08-17 12:12:59 +02:00
|
|
|
path_span,
|
2022-08-10 11:31:31 +02:00
|
|
|
Level::Error,
|
|
|
|
format!("attribute `{attr_name}` contains a '-' character"),
|
|
|
|
)
|
|
|
|
.help("replace any '-'s with '_'s")
|
|
|
|
.emit();
|
|
|
|
}
|
|
|
|
|
2022-05-23 18:24:55 +01:00
|
|
|
constants.extend(quote! {
|
2022-05-24 15:09:47 +01:00
|
|
|
pub const #snake_name: crate::SubdiagnosticMessage =
|
|
|
|
crate::SubdiagnosticMessage::FluentAttr(
|
|
|
|
std::borrow::Cow::Borrowed(#attr_name)
|
2022-05-23 18:24:55 +01:00
|
|
|
);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Err(errs) = bundle.add_resource(resource) {
|
|
|
|
for e in errs {
|
|
|
|
match e {
|
|
|
|
FluentError::Overriding { kind, id } => {
|
|
|
|
Diagnostic::spanned(
|
2022-08-17 12:12:59 +02:00
|
|
|
path_span,
|
2022-05-23 18:24:55 +01:00
|
|
|
Level::Error,
|
|
|
|
format!("overrides existing {}: `{}`", kind, id),
|
|
|
|
)
|
|
|
|
.span_help(previous_defns[&id], "previously defined in this resource")
|
|
|
|
.emit();
|
|
|
|
}
|
|
|
|
FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
includes.extend(quote! { include_str!(#relative_ftl_path), });
|
|
|
|
|
2022-10-21 12:25:25 +02:00
|
|
|
generated.extend(constants);
|
2022-05-23 18:24:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
quote! {
|
|
|
|
#[allow(non_upper_case_globals)]
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub mod fluent_generated {
|
|
|
|
pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
|
|
|
|
#includes
|
|
|
|
];
|
|
|
|
|
|
|
|
#generated
|
2022-06-23 14:51:44 +01:00
|
|
|
|
|
|
|
pub mod _subdiag {
|
|
|
|
pub const help: crate::SubdiagnosticMessage =
|
|
|
|
crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
|
2022-07-11 18:46:24 +01:00
|
|
|
pub const note: crate::SubdiagnosticMessage =
|
|
|
|
crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
|
|
|
|
pub const warn: crate::SubdiagnosticMessage =
|
|
|
|
crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
|
2022-06-23 14:51:44 +01:00
|
|
|
pub const label: crate::SubdiagnosticMessage =
|
|
|
|
crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
|
|
|
|
pub const suggestion: crate::SubdiagnosticMessage =
|
|
|
|
crate::SubdiagnosticMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
|
|
|
|
}
|
2022-05-23 18:24:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.into()
|
|
|
|
}
|