Skip to content

Commit 7e6b617

Browse files
committed
macros: introduce fluent_messages macro
Adds a new `fluent_messages` macro which performs compile-time validation of the compiler's Fluent resources (i.e. that the resources parse and don't multiply define the same messages) and generates constants that make using those messages in diagnostics more ergonomic. For example, given the following invocation of the macro.. ```ignore (rust) fluent_messages! { typeck => "./typeck.ftl", } ``` ..where `typeck.ftl` has the following contents.. ```fluent typeck-field-multiply-specified-in-initializer = field `{$ident}` specified more than once .label = used more than once .label-previous-use = first use of `{$ident}` ``` ...then the macro parse the Fluent resource, emitting a diagnostic if it fails to do so, and will generate the following code: ```ignore (rust) pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[ include_str!("./typeck.ftl"), ]; mod fluent_generated { mod typeck { pub const field_multiply_specified_in_initializer: DiagnosticMessage = DiagnosticMessage::fluent("typeck-field-multiply-specified-in-initializer"); pub const field_multiply_specified_in_initializer_label_previous_use: DiagnosticMessage = DiagnosticMessage::fluent_attr( "typeck-field-multiply-specified-in-initializer", "previous-use-label" ); } } ``` When emitting a diagnostic, the generated constants can be used as follows: ```ignore (rust) let mut err = sess.struct_span_err( span, fluent::typeck::field_multiply_specified_in_initializer ); err.span_default_label(span); err.span_label( previous_use_span, fluent::typeck::field_multiply_specified_in_initializer_label_previous_use ); err.emit(); ``` Signed-off-by: David Wood <david.wood@huawei.com>
1 parent effb56e commit 7e6b617

File tree

13 files changed

+440
-5
lines changed

13 files changed

+440
-5
lines changed

Cargo.lock

+4
Original file line numberDiff line numberDiff line change
@@ -4008,10 +4008,14 @@ dependencies = [
40084008
name = "rustc_macros"
40094009
version = "0.1.0"
40104010
dependencies = [
4011+
"annotate-snippets",
4012+
"fluent-bundle",
4013+
"fluent-syntax",
40114014
"proc-macro2",
40124015
"quote",
40134016
"syn",
40144017
"synstructure",
4018+
"unic-langid",
40154019
]
40164020

40174021
[[package]]

compiler/rustc_error_messages/src/lib.rs

+8-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use fluent_bundle::FluentResource;
77
use fluent_syntax::parser::ParserError;
88
use rustc_data_structures::sync::Lrc;
9-
use rustc_macros::{Decodable, Encodable};
9+
use rustc_macros::{fluent_messages, Decodable, Encodable};
1010
use rustc_span::Span;
1111
use std::borrow::Cow;
1212
use std::error::Error;
@@ -29,8 +29,13 @@ use intl_memoizer::IntlLangMemoizer;
2929
pub use fluent_bundle::{FluentArgs, FluentError, FluentValue};
3030
pub use unic_langid::{langid, LanguageIdentifier};
3131

32-
pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] =
33-
&[include_str!("../locales/en-US/typeck.ftl"), include_str!("../locales/en-US/parser.ftl")];
32+
// Generates `DEFAULT_LOCALE_RESOURCES` static and `fluent_generated` module.
33+
fluent_messages! {
34+
parser => "../locales/en-US/parser.ftl",
35+
typeck => "../locales/en-US/typeck.ftl",
36+
}
37+
38+
pub use fluent_generated::{self as fluent, DEFAULT_LOCALE_RESOURCES};
3439

3540
pub type FluentBundle = fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>;
3641

compiler/rustc_errors/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ use rustc_data_structures::stable_hasher::StableHasher;
3131
use rustc_data_structures::sync::{self, Lock, Lrc};
3232
use rustc_data_structures::AtomicRef;
3333
pub use rustc_error_messages::{
34-
fallback_fluent_bundle, fluent_bundle, DiagnosticMessage, FluentBundle, LanguageIdentifier,
35-
LazyFallbackBundle, MultiSpan, SpanLabel, DEFAULT_LOCALE_RESOURCES,
34+
fallback_fluent_bundle, fluent, fluent_bundle, DiagnosticMessage, FluentBundle,
35+
LanguageIdentifier, LazyFallbackBundle, MultiSpan, SpanLabel, DEFAULT_LOCALE_RESOURCES,
3636
};
3737
pub use rustc_lint_defs::{pluralize, Applicability};
3838
use rustc_span::source_map::SourceMap;

compiler/rustc_macros/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ edition = "2021"
77
proc-macro = true
88

99
[dependencies]
10+
annotate-snippets = "0.8.0"
11+
fluent-bundle = "0.15.2"
12+
fluent-syntax = "0.11"
1013
synstructure = "0.12.1"
1114
syn = { version = "1", features = ["full"] }
1215
proc-macro2 = "1"
1316
quote = "1"
17+
unic-langid = { version = "0.9.0", features = ["macros"] }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use annotate_snippets::{
2+
display_list::DisplayList,
3+
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
4+
};
5+
use fluent_bundle::{FluentBundle, FluentError, FluentResource};
6+
use fluent_syntax::{
7+
ast::{Attribute, Entry, Identifier, Message},
8+
parser::ParserError,
9+
};
10+
use proc_macro::{Diagnostic, Level, Span};
11+
use proc_macro2::TokenStream;
12+
use quote::quote;
13+
use std::{
14+
collections::HashMap,
15+
fs::File,
16+
io::Read,
17+
path::{Path, PathBuf},
18+
};
19+
use syn::{
20+
parse::{Parse, ParseStream},
21+
parse_macro_input,
22+
punctuated::Punctuated,
23+
token, Ident, LitStr, Result,
24+
};
25+
use unic_langid::langid;
26+
27+
struct Resource {
28+
ident: Ident,
29+
#[allow(dead_code)]
30+
fat_arrow_token: token::FatArrow,
31+
resource: LitStr,
32+
}
33+
34+
impl Parse for Resource {
35+
fn parse(input: ParseStream<'_>) -> Result<Self> {
36+
Ok(Resource {
37+
ident: input.parse()?,
38+
fat_arrow_token: input.parse()?,
39+
resource: input.parse()?,
40+
})
41+
}
42+
}
43+
44+
struct Resources(Punctuated<Resource, token::Comma>);
45+
46+
impl Parse for Resources {
47+
fn parse(input: ParseStream<'_>) -> Result<Self> {
48+
let mut resources = Punctuated::new();
49+
loop {
50+
if input.is_empty() || input.peek(token::Brace) {
51+
break;
52+
}
53+
let value = input.parse()?;
54+
resources.push_value(value);
55+
if !input.peek(token::Comma) {
56+
break;
57+
}
58+
let punct = input.parse()?;
59+
resources.push_punct(punct);
60+
}
61+
Ok(Resources(resources))
62+
}
63+
}
64+
65+
/// Helper function for returning an absolute path for macro-invocation relative file paths.
66+
///
67+
/// If the input is already absolute, then the input is returned. If the input is not absolute,
68+
/// then it is appended to the directory containing the source file with this macro invocation.
69+
fn invocation_relative_path_to_absolute(span: Span, path: &str) -> PathBuf {
70+
let path = Path::new(path);
71+
if path.is_absolute() {
72+
path.to_path_buf()
73+
} else {
74+
// `/a/b/c/foo/bar.rs` contains the current macro invocation
75+
let mut source_file_path = span.source_file().path();
76+
// `/a/b/c/foo/`
77+
source_file_path.pop();
78+
// `/a/b/c/foo/../locales/en-US/example.ftl`
79+
source_file_path.push(path);
80+
source_file_path
81+
}
82+
}
83+
84+
/// See [rustc_macros::fluent_messages].
85+
pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
86+
let resources = parse_macro_input!(input as Resources);
87+
88+
// Cannot iterate over individual messages in a bundle, so do that using the
89+
// `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
90+
// messages in the resources.
91+
let mut bundle = FluentBundle::new(vec![langid!("en-US")]);
92+
93+
// Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
94+
// diagnostics.
95+
let mut previous_defns = HashMap::new();
96+
97+
let mut includes = TokenStream::new();
98+
let mut generated = TokenStream::new();
99+
for res in resources.0 {
100+
let ident_span = res.ident.span().unwrap();
101+
let path_span = res.resource.span().unwrap();
102+
103+
let relative_ftl_path = res.resource.value();
104+
let absolute_ftl_path =
105+
invocation_relative_path_to_absolute(ident_span, &relative_ftl_path);
106+
// As this macro also outputs an `include_str!` for this file, the macro will always be
107+
// re-executed when the file changes.
108+
let mut resource_file = match File::open(absolute_ftl_path) {
109+
Ok(resource_file) => resource_file,
110+
Err(e) => {
111+
Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
112+
.note(e.to_string())
113+
.emit();
114+
continue;
115+
}
116+
};
117+
let mut resource_contents = String::new();
118+
if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
119+
Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
120+
.note(e.to_string())
121+
.emit();
122+
continue;
123+
}
124+
let resource = match FluentResource::try_new(resource_contents) {
125+
Ok(resource) => resource,
126+
Err((this, errs)) => {
127+
Diagnostic::spanned(path_span, Level::Error, "could not parse Fluent resource")
128+
.help("see additional errors emitted")
129+
.emit();
130+
for ParserError { pos, slice: _, kind } in errs {
131+
let mut err = kind.to_string();
132+
// Entirely unnecessary string modification so that the error message starts
133+
// with a lowercase as rustc errors do.
134+
err.replace_range(
135+
0..1,
136+
&err.chars().next().unwrap().to_lowercase().to_string(),
137+
);
138+
139+
let line_starts: Vec<usize> = std::iter::once(0)
140+
.chain(
141+
this.source()
142+
.char_indices()
143+
.filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
144+
)
145+
.collect();
146+
let line_start = line_starts
147+
.iter()
148+
.enumerate()
149+
.map(|(line, idx)| (line + 1, idx))
150+
.filter(|(_, idx)| **idx <= pos.start)
151+
.last()
152+
.unwrap()
153+
.0;
154+
155+
let snippet = Snippet {
156+
title: Some(Annotation {
157+
label: Some(&err),
158+
id: None,
159+
annotation_type: AnnotationType::Error,
160+
}),
161+
footer: vec![],
162+
slices: vec![Slice {
163+
source: this.source(),
164+
line_start,
165+
origin: Some(&relative_ftl_path),
166+
fold: true,
167+
annotations: vec![SourceAnnotation {
168+
label: "",
169+
annotation_type: AnnotationType::Error,
170+
range: (pos.start, pos.end - 1),
171+
}],
172+
}],
173+
opt: Default::default(),
174+
};
175+
let dl = DisplayList::from(snippet);
176+
eprintln!("{}\n", dl);
177+
}
178+
continue;
179+
}
180+
};
181+
182+
let mut constants = TokenStream::new();
183+
for entry in resource.entries() {
184+
let span = res.ident.span();
185+
if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry {
186+
let _ = previous_defns.entry(name.to_string()).or_insert(ident_span);
187+
188+
// `typeck-foo-bar` => `foo_bar`
189+
let snake_name = Ident::new(
190+
&name.replace(&format!("{}-", res.ident), "").replace("-", "_"),
191+
span,
192+
);
193+
constants.extend(quote! {
194+
pub const #snake_name: crate::DiagnosticMessage =
195+
crate::DiagnosticMessage::FluentIdentifier(
196+
std::borrow::Cow::Borrowed(#name),
197+
None
198+
);
199+
});
200+
201+
for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
202+
let attr_snake_name = attr_name.replace("-", "_");
203+
let snake_name = Ident::new(&format!("{snake_name}_{attr_snake_name}"), span);
204+
constants.extend(quote! {
205+
pub const #snake_name: crate::DiagnosticMessage =
206+
crate::DiagnosticMessage::FluentIdentifier(
207+
std::borrow::Cow::Borrowed(#name),
208+
Some(std::borrow::Cow::Borrowed(#attr_name))
209+
);
210+
});
211+
}
212+
}
213+
}
214+
215+
if let Err(errs) = bundle.add_resource(resource) {
216+
for e in errs {
217+
match e {
218+
FluentError::Overriding { kind, id } => {
219+
Diagnostic::spanned(
220+
ident_span,
221+
Level::Error,
222+
format!("overrides existing {}: `{}`", kind, id),
223+
)
224+
.span_help(previous_defns[&id], "previously defined in this resource")
225+
.emit();
226+
}
227+
FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
228+
}
229+
}
230+
}
231+
232+
includes.extend(quote! { include_str!(#relative_ftl_path), });
233+
234+
let ident = res.ident;
235+
generated.extend(quote! {
236+
pub mod #ident {
237+
#constants
238+
}
239+
});
240+
}
241+
242+
quote! {
243+
#[allow(non_upper_case_globals)]
244+
#[doc(hidden)]
245+
pub mod fluent_generated {
246+
pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
247+
#includes
248+
];
249+
250+
#generated
251+
}
252+
}
253+
.into()
254+
}

compiler/rustc_macros/src/diagnostics/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod diagnostic;
22
mod error;
3+
mod fluent;
34
mod subdiagnostic;
45
mod utils;
56

67
use diagnostic::SessionDiagnosticDerive;
8+
pub(crate) use fluent::fluent_messages;
79
use proc_macro2::TokenStream;
810
use quote::format_ident;
911
use subdiagnostic::SessionSubdiagnosticDerive;

0 commit comments

Comments
 (0)