1 use annotate_snippets
::{
2 display_list
::DisplayList
,
3 snippet
::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}
,
5 use fluent_bundle
::{FluentBundle, FluentError, FluentResource}
;
7 ast
::{Attribute, Entry, Identifier, Message}
,
10 use proc_macro
::{Diagnostic, Level, Span}
;
11 use proc_macro2
::TokenStream
;
14 collections
::{HashMap, HashSet}
,
17 path
::{Path, PathBuf}
,
20 parse
::{Parse, ParseStream}
,
22 punctuated
::Punctuated
,
23 token
, Ident
, LitStr
, Result
,
25 use unic_langid
::langid
;
30 fat_arrow_token
: token
::FatArrow
,
34 impl Parse
for Resource
{
35 fn parse(input
: ParseStream
<'_
>) -> Result
<Self> {
37 ident
: input
.parse()?
,
38 fat_arrow_token
: input
.parse()?
,
39 resource
: input
.parse()?
,
44 struct Resources(Punctuated
<Resource
, token
::Comma
>);
46 impl Parse
for Resources
{
47 fn parse(input
: ParseStream
<'_
>) -> Result
<Self> {
48 let mut resources
= Punctuated
::new();
50 if input
.is_empty() || input
.peek(token
::Brace
) {
53 let value
= input
.parse()?
;
54 resources
.push_value(value
);
55 if !input
.peek(token
::Comma
) {
58 let punct
= input
.parse()?
;
59 resources
.push_punct(punct
);
61 Ok(Resources(resources
))
65 /// Helper function for returning an absolute path for macro-invocation relative file paths.
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() {
74 // `/a/b/c/foo/bar.rs` contains the current macro invocation
75 let mut source_file_path
= span
.source_file().path();
77 source_file_path
.pop();
78 // `/a/b/c/foo/../locales/en-US/example.ftl`
79 source_file_path
.push(path
);
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
);
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")]);
93 // Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
95 let mut previous_defns
= HashMap
::new();
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();
103 // Set of Fluent attribute names already output, to avoid duplicate type errors - any given
104 // constant created for a given attribute is the same.
105 let mut previous_attrs
= HashSet
::new();
107 let relative_ftl_path
= res
.resource
.value();
108 let absolute_ftl_path
=
109 invocation_relative_path_to_absolute(ident_span
, &relative_ftl_path
);
110 // As this macro also outputs an `include_str!` for this file, the macro will always be
111 // re-executed when the file changes.
112 let mut resource_file
= match File
::open(absolute_ftl_path
) {
113 Ok(resource_file
) => resource_file
,
115 Diagnostic
::spanned(path_span
, Level
::Error
, "could not open Fluent resource")
121 let mut resource_contents
= String
::new();
122 if let Err(e
) = resource_file
.read_to_string(&mut resource_contents
) {
123 Diagnostic
::spanned(path_span
, Level
::Error
, "could not read Fluent resource")
128 let resource
= match FluentResource
::try_new(resource_contents
) {
129 Ok(resource
) => resource
,
130 Err((this
, errs
)) => {
131 Diagnostic
::spanned(path_span
, Level
::Error
, "could not parse Fluent resource")
132 .help("see additional errors emitted")
134 for ParserError { pos, slice: _, kind }
in errs
{
135 let mut err
= kind
.to_string();
136 // Entirely unnecessary string modification so that the error message starts
137 // with a lowercase as rustc errors do.
140 &err
.chars().next().unwrap().to_lowercase().to_string(),
143 let line_starts
: Vec
<usize> = std
::iter
::once(0)
147 .filter_map(|(i
, c
)| Some(i
+ 1).filter(|_
| c
== '
\n'
)),
150 let line_start
= line_starts
153 .map(|(line
, idx
)| (line
+ 1, idx
))
154 .filter(|(_
, idx
)| **idx
<= pos
.start
)
159 let snippet
= Snippet
{
160 title
: Some(Annotation
{
163 annotation_type
: AnnotationType
::Error
,
167 source
: this
.source(),
169 origin
: Some(&relative_ftl_path
),
171 annotations
: vec
![SourceAnnotation
{
173 annotation_type
: AnnotationType
::Error
,
174 range
: (pos
.start
, pos
.end
- 1),
177 opt
: Default
::default(),
179 let dl
= DisplayList
::from(snippet
);
180 eprintln
!("{}\n", dl
);
186 let mut constants
= TokenStream
::new();
187 for entry
in resource
.entries() {
188 let span
= res
.ident
.span();
189 if let Entry
::Message(Message { id: Identifier { name }
, attributes
, .. }) = entry
{
190 let _
= previous_defns
.entry(name
.to_string()).or_insert(ident_span
);
192 // `typeck-foo-bar` => `foo_bar` (in `typeck.ftl`)
193 // `const-eval-baz` => `baz` (in `const_eval.ftl`)
194 let snake_name
= Ident
::new(
195 // FIXME: should probably trim prefix, not replace all occurrences
197 .replace(&format
!("{}-", res
.ident
).replace('_'
, "-"), "")
201 constants
.extend(quote
! {
202 pub const #snake_name: crate::DiagnosticMessage =
203 crate::DiagnosticMessage
::FluentIdentifier(
204 std
::borrow
::Cow
::Borrowed(#name),
209 for Attribute { id: Identifier { name: attr_name }
, .. } in attributes
{
210 let snake_name
= Ident
::new(&attr_name
.replace('
-'
, "_"), span
);
211 if !previous_attrs
.insert(snake_name
.clone()) {
215 constants
.extend(quote
! {
216 pub const #snake_name: crate::SubdiagnosticMessage =
217 crate::SubdiagnosticMessage
::FluentAttr(
218 std
::borrow
::Cow
::Borrowed(#attr_name)
225 if let Err(errs
) = bundle
.add_resource(resource
) {
228 FluentError
::Overriding { kind, id }
=> {
232 format
!("overrides existing {}: `{}`", kind
, id
),
234 .span_help(previous_defns
[&id
], "previously defined in this resource")
237 FluentError
::ResolverError(_
) | FluentError
::ParserError(_
) => unreachable
!(),
242 includes
.extend(quote
! { include_str!(#relative_ftl_path), }
);
244 let ident
= res
.ident
;
245 generated
.extend(quote
! {
253 #[allow(non_upper_case_globals)]
255 pub mod fluent_generated
{
256 pub static DEFAULT_LOCALE_RESOURCES
: &'
static [&'
static str] = &[
263 pub const help
: crate::SubdiagnosticMessage
=
264 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("help"));
265 pub const note
: crate::SubdiagnosticMessage
=
266 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("note"));
267 pub const warn
: crate::SubdiagnosticMessage
=
268 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("warn"));
269 pub const label
: crate::SubdiagnosticMessage
=
270 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("label"));
271 pub const suggestion
: crate::SubdiagnosticMessage
=
272 crate::SubdiagnosticMessage
::FluentAttr(std
::borrow
::Cow
::Borrowed("suggestion"));