1 use annotate_snippets
::{
2 display_list
::DisplayList
,
3 snippet
::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}
,
5 use fluent_bundle
::{FluentBundle, FluentError, FluentResource}
;
8 Attribute
, Entry
, Expression
, Identifier
, InlineExpression
, Message
, Pattern
,
13 use proc_macro
::{Diagnostic, Level, Span}
;
14 use proc_macro2
::TokenStream
;
17 collections
::{HashMap, HashSet}
,
19 path
::{Path, PathBuf}
,
21 use syn
::{parse_macro_input, Ident, LitStr}
;
22 use unic_langid
::langid
;
24 /// Helper function for returning an absolute path for macro-invocation relative file paths.
26 /// If the input is already absolute, then the input is returned. If the input is not absolute,
27 /// then it is appended to the directory containing the source file with this macro invocation.
28 fn invocation_relative_path_to_absolute(span
: Span
, path
: &str) -> PathBuf
{
29 let path
= Path
::new(path
);
30 if path
.is_absolute() {
33 // `/a/b/c/foo/bar.rs` contains the current macro invocation
34 let mut source_file_path
= span
.source_file().path();
36 source_file_path
.pop();
37 // `/a/b/c/foo/../locales/en-US/example.ftl`
38 source_file_path
.push(path
);
43 /// Tokens to be returned when the macro cannot proceed.
44 fn failed(crate_name
: &Ident
) -> proc_macro
::TokenStream
{
46 pub static DEFAULT_LOCALE_RESOURCE
: &'
static str = "";
48 #[allow(non_upper_case_globals)]
50 pub(crate) mod fluent_generated
{
55 pub const help
: crate::SubdiagnosticMessage
=
56 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("help"));
57 pub const note
: crate::SubdiagnosticMessage
=
58 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("note"));
59 pub const warn
: crate::SubdiagnosticMessage
=
60 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("warn"));
61 pub const label
: crate::SubdiagnosticMessage
=
62 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("label"));
63 pub const suggestion
: crate::SubdiagnosticMessage
=
64 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("suggestion"));
71 /// See [rustc_fluent_macro::fluent_messages].
72 pub(crate) fn fluent_messages(input
: proc_macro
::TokenStream
) -> proc_macro
::TokenStream
{
73 let crate_name
= std
::env
::var("CARGO_PKG_NAME")
74 // If `CARGO_PKG_NAME` is missing, then we're probably running in a test, so use
76 .unwrap_or_else(|_
| "no_crate".to_string())
77 .replace("rustc_", "");
79 // Cannot iterate over individual messages in a bundle, so do that using the
80 // `FluentResource` instead. Construct a bundle anyway to find out if there are conflicting
81 // messages in the resources.
82 let mut bundle
= FluentBundle
::new(vec
![langid
!("en-US")]);
84 // Set of Fluent attribute names already output, to avoid duplicate type errors - any given
85 // constant created for a given attribute is the same.
86 let mut previous_attrs
= HashSet
::new();
88 let resource_str
= parse_macro_input
!(input
as LitStr
);
89 let resource_span
= resource_str
.span().unwrap();
90 let relative_ftl_path
= resource_str
.value();
91 let absolute_ftl_path
= invocation_relative_path_to_absolute(resource_span
, &relative_ftl_path
);
93 let crate_name
= Ident
::new(&crate_name
, resource_str
.span());
95 // As this macro also outputs an `include_str!` for this file, the macro will always be
96 // re-executed when the file changes.
97 let resource_contents
= match read_to_string(absolute_ftl_path
) {
98 Ok(resource_contents
) => resource_contents
,
103 format
!("could not open Fluent resource: {e}"),
106 return failed(&crate_name
);
110 for esc
in ["\\n", "\\\"", "\\'"] {
111 for _
in resource_contents
.matches(esc
) {
113 Diagnostic
::spanned(resource_span
, Level
::Error
, format
!("invalid escape `{esc}` in Fluent resource"))
114 .note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
119 return failed(&crate_name
);
122 let resource
= match FluentResource
::try_new(resource_contents
) {
123 Ok(resource
) => resource
,
124 Err((this
, errs
)) => {
125 Diagnostic
::spanned(resource_span
, Level
::Error
, "could not parse Fluent resource")
126 .help("see additional errors emitted")
128 for ParserError { pos, slice: _, kind }
in errs
{
129 let mut err
= kind
.to_string();
130 // Entirely unnecessary string modification so that the error message starts
131 // with a lowercase as rustc errors do.
132 err
.replace_range(0..1, &err
.chars().next().unwrap().to_lowercase().to_string());
134 let line_starts
: Vec
<usize> = std
::iter
::once(0)
138 .filter_map(|(i
, c
)| Some(i
+ 1).filter(|_
| c
== '
\n'
)),
141 let line_start
= line_starts
144 .map(|(line
, idx
)| (line
+ 1, idx
))
145 .filter(|(_
, idx
)| **idx
<= pos
.start
)
150 let snippet
= Snippet
{
151 title
: Some(Annotation
{
154 annotation_type
: AnnotationType
::Error
,
158 source
: this
.source(),
160 origin
: Some(&relative_ftl_path
),
162 annotations
: vec
![SourceAnnotation
{
164 annotation_type
: AnnotationType
::Error
,
165 range
: (pos
.start
, pos
.end
- 1),
168 opt
: Default
::default(),
170 let dl
= DisplayList
::from(snippet
);
174 return failed(&crate_name
);
178 let mut constants
= TokenStream
::new();
179 let mut previous_defns
= HashMap
::new();
180 let mut message_refs
= Vec
::new();
181 for entry
in resource
.entries() {
182 if let Entry
::Message(msg
) = entry
{
183 let Message { id: Identifier { name }
, attributes
, value
, .. } = msg
;
184 let _
= previous_defns
.entry(name
.to_string()).or_insert(resource_span
);
185 if name
.contains('
-'
) {
189 format
!("name `{name}` contains a '-' character"),
191 .help("replace any '-'s with '_'s")
195 if let Some(Pattern { elements }
) = value
{
196 for elt
in elements
{
197 if let PatternElement
::Placeable
{
199 Expression
::Inline(InlineExpression
::MessageReference { id, .. }
),
202 message_refs
.push((id
.name
, *name
));
207 // `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`)
208 // `const_eval_baz` => `baz` (in `const_eval.ftl`)
209 // `const-eval-hyphen-having` => `hyphen_having` (in `const_eval.ftl`)
210 // The last case we error about above, but we want to fall back gracefully
211 // so that only the error is being emitted and not also one about the macro
213 let crate_prefix
= format
!("{crate_name}_");
215 let snake_name
= name
.replace('
-'
, "_");
216 if !snake_name
.starts_with(&crate_prefix
) {
220 format
!("name `{name}` does not start with the crate name"),
223 "prepend `{crate_prefix}` to the slug name: `{crate_prefix}{snake_name}`"
227 let snake_name
= Ident
::new(&snake_name
, resource_str
.span());
229 if !previous_attrs
.insert(snake_name
.clone()) {
234 format
!("Constant referring to Fluent message `{name}` from `{crate_name}`");
235 constants
.extend(quote
! {
237 pub const #snake_name: crate::DiagnosticMessage =
238 crate::DiagnosticMessage
::FluentIdentifier(
239 std
::borrow
::Cow
::Borrowed(#name),
244 for Attribute { id: Identifier { name: attr_name }
, .. } in attributes
{
245 let snake_name
= Ident
::new(
246 &format
!("{}{}", &crate_prefix
, &attr_name
.replace('
-'
, "_")),
249 if !previous_attrs
.insert(snake_name
.clone()) {
253 if attr_name
.contains('
-'
) {
257 format
!("attribute `{attr_name}` contains a '-' character"),
259 .help("replace any '-'s with '_'s")
264 "Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
266 constants
.extend(quote
! {
268 pub const #snake_name: crate::SubdiagnosticMessage =
269 crate::SubdiagnosticMessage
::FluentAttr(
270 std
::borrow
::Cow
::Borrowed(#attr_name)
275 // Record variables referenced by these messages so we can produce
276 // tests in the derive diagnostics to validate them.
277 let ident
= quote
::format_ident
!("{snake_name}_refs");
278 let vrefs
= variable_references(msg
);
279 constants
.extend(quote
! {
281 pub const #ident: &[&str] = &[#(#vrefs),*];
286 for (mref
, name
) in message_refs
.into_iter() {
287 if !previous_defns
.contains_key(mref
) {
291 format
!("referenced message `{mref}` does not exist (in message `{name}`)"),
293 .help(&format
!("you may have meant to use a variable reference (`{{${mref}}}`)"))
298 if let Err(errs
) = bundle
.add_resource(resource
) {
301 FluentError
::Overriding { kind, id }
=> {
305 format
!("overrides existing {kind}: `{id}`"),
309 FluentError
::ResolverError(_
) | FluentError
::ParserError(_
) => unreachable
!(),
315 /// Raw content of Fluent resource for this crate, generated by `fluent_messages` macro,
316 /// imported by `rustc_driver` to include all crates' resources in one bundle.
317 pub static DEFAULT_LOCALE_RESOURCE
: &'
static str = include_str
!(#relative_ftl_path);
319 #[allow(non_upper_case_globals)]
321 /// Auto-generated constants for type-checked references to Fluent messages.
322 pub(crate) mod fluent_generated
{
325 /// Constants expected to exist by the diagnostic derive macros to use as default Fluent
326 /// identifiers for different subdiagnostic kinds.
328 /// Default for `#[help]`
329 pub const help
: crate::SubdiagnosticMessage
=
330 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("help"));
331 /// Default for `#[note]`
332 pub const note
: crate::SubdiagnosticMessage
=
333 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("note"));
334 /// Default for `#[warn]`
335 pub const warn
: crate::SubdiagnosticMessage
=
336 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("warn"));
337 /// Default for `#[label]`
338 pub const label
: crate::SubdiagnosticMessage
=
339 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("label"));
340 /// Default for `#[suggestion]`
341 pub const suggestion
: crate::SubdiagnosticMessage
=
342 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("suggestion"));
349 fn variable_references
<'a
>(msg
: &Message
<&'a
str>) -> Vec
<&'a
str> {
350 let mut refs
= vec
![];
351 if let Some(Pattern { elements }
) = &msg
.value
{
352 for elt
in elements
{
353 if let PatternElement
::Placeable
{
354 expression
: Expression
::Inline(InlineExpression
::VariableReference { id }
),
361 for attr
in &msg
.attributes
{
362 for elt
in &attr
.value
.elements
{
363 if let PatternElement
::Placeable
{
364 expression
: Expression
::Inline(InlineExpression
::VariableReference { id }
),