Remove rustc_fluent_macro

This commit is contained in:
Jonathan Brouwer 2026-02-06 19:32:52 +01:00
parent c814f76c06
commit f35d734d3f
No known key found for this signature in database
GPG key ID: 13619B051B673C52
17 changed files with 5 additions and 434 deletions

View file

@ -3888,19 +3888,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc_fluent_macro"
version = "0.0.0"
dependencies = [
"annotate-snippets 0.11.5",
"fluent-bundle",
"fluent-syntax",
"proc-macro2",
"quote",
"syn 2.0.110",
"unic-langid",
]
[[package]]
name = "rustc_fs_util"
version = "0.0.0"

View file

@ -1,18 +0,0 @@
[package]
name = "rustc_fluent_macro"
version = "0.0.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
# tidy-alphabetical-start
annotate-snippets = "0.11"
fluent-bundle = "0.16"
fluent-syntax = "0.12"
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
unic-langid = { version = "0.9.0", features = ["macros"] }
# tidy-alphabetical-end

View file

@ -1,315 +0,0 @@
use std::collections::{HashMap, HashSet};
use std::fs::read_to_string;
use std::path::{Path, PathBuf};
use annotate_snippets::{Renderer, Snippet};
use fluent_bundle::{FluentBundle, FluentError, FluentResource};
use fluent_syntax::ast::{
Attribute, Entry, Expression, Identifier, InlineExpression, Message, Pattern, PatternElement,
};
use fluent_syntax::parser::ParserError;
use proc_macro::tracked::path;
use proc_macro::{Diagnostic, Level, Span};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Ident, LitStr, parse_macro_input};
use unic_langid::langid;
/// 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.local_file().unwrap();
// `/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
}
}
/// Final tokens.
fn finish(body: TokenStream, resource: TokenStream) -> proc_macro::TokenStream {
quote! {
/// Raw content of Fluent resource for this crate, generated by `fluent_messages` macro,
/// imported by `rustc_driver` to include all crates' resources in one bundle.
pub static DEFAULT_LOCALE_RESOURCE: &'static str = #resource;
#[allow(non_upper_case_globals)]
#[doc(hidden)]
/// Auto-generated constants for type-checked references to Fluent messages.
pub(crate) mod fluent_generated {
#body
/// Constants expected to exist by the diagnostic derive macros to use as default Fluent
/// identifiers for different subdiagnostic kinds.
pub mod _subdiag {
/// Default for `#[help]`
pub const help: rustc_errors::SubdiagMessage =
rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("help"));
/// Default for `#[note]`
pub const note: rustc_errors::SubdiagMessage =
rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("note"));
/// Default for `#[warn]`
pub const warn: rustc_errors::SubdiagMessage =
rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("warn"));
/// Default for `#[label]`
pub const label: rustc_errors::SubdiagMessage =
rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("label"));
/// Default for `#[suggestion]`
pub const suggestion: rustc_errors::SubdiagMessage =
rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed("suggestion"));
}
}
}
.into()
}
/// Tokens to be returned when the macro cannot proceed.
fn failed(crate_name: &Ident) -> proc_macro::TokenStream {
finish(quote! { pub mod #crate_name {} }, quote! { "" })
}
/// See [rustc_fluent_macro::fluent_messages].
pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let crate_name = std::env::var("CARGO_CRATE_NAME")
// If `CARGO_CRATE_NAME` is missing, then we're probably running in a test, so use
// `no_crate`.
.unwrap_or_else(|_| "no_crate".to_string())
.replace("rustc_", "");
// 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")]);
// 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();
let resource_str = parse_macro_input!(input as LitStr);
let resource_span = resource_str.span().unwrap();
let relative_ftl_path = resource_str.value();
let absolute_ftl_path = invocation_relative_path_to_absolute(resource_span, &relative_ftl_path);
let crate_name = Ident::new(&crate_name, resource_str.span());
path(absolute_ftl_path.to_str().unwrap());
let resource_contents = match read_to_string(absolute_ftl_path) {
Ok(resource_contents) => resource_contents,
Err(e) => {
Diagnostic::spanned(
resource_span,
Level::Error,
format!("could not open Fluent resource: {e}"),
)
.emit();
return failed(&crate_name);
}
};
let mut bad = false;
for esc in ["\\n", "\\\"", "\\'"] {
for _ in resource_contents.matches(esc) {
bad = true;
Diagnostic::spanned(resource_span, Level::Error, format!("invalid escape `{esc}` in Fluent resource"))
.note("Fluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)")
.emit();
}
}
if bad {
return failed(&crate_name);
}
let resource = match FluentResource::try_new(resource_contents) {
Ok(resource) => resource,
Err((this, errs)) => {
Diagnostic::spanned(resource_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 message = annotate_snippets::Level::Error.title(&err).snippet(
Snippet::source(this.source())
.origin(&relative_ftl_path)
.fold(true)
.annotation(annotate_snippets::Level::Error.span(pos.start..pos.end - 1)),
);
let renderer = Renderer::plain();
eprintln!("{}\n", renderer.render(message));
}
return failed(&crate_name);
}
};
let mut constants = TokenStream::new();
let mut previous_defns = HashMap::new();
let mut message_refs = Vec::new();
for entry in resource.entries() {
if let Entry::Message(msg) = entry {
let Message { id: Identifier { name }, attributes, value, .. } = msg;
let _ = previous_defns.entry(name.to_string()).or_insert(resource_span);
if name.contains('-') {
Diagnostic::spanned(
resource_span,
Level::Error,
format!("name `{name}` contains a '-' character"),
)
.help("replace any '-'s with '_'s")
.emit();
}
if let Some(Pattern { elements }) = value {
for elt in elements {
if let PatternElement::Placeable {
expression:
Expression::Inline(InlineExpression::MessageReference { id, .. }),
} = elt
{
message_refs.push((id.name, *name));
}
}
}
// `typeck_foo_bar` => `foo_bar` (in `typeck.ftl`)
// `const_eval_baz` => `baz` (in `const_eval.ftl`)
// `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.
let crate_prefix = format!("{crate_name}_");
let snake_name = name.replace('-', "_");
if !snake_name.starts_with(&crate_prefix) {
Diagnostic::spanned(
resource_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();
};
let snake_name = Ident::new(&snake_name, resource_str.span());
if !previous_attrs.insert(snake_name.clone()) {
continue;
}
let docstr =
format!("Constant referring to Fluent message `{name}` from `{crate_name}`");
constants.extend(quote! {
#[doc = #docstr]
pub const #snake_name: rustc_errors::DiagMessage =
rustc_errors::DiagMessage::FluentIdentifier(
std::borrow::Cow::Borrowed(#name),
None
);
});
for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
let snake_name = Ident::new(
&format!("{crate_prefix}{}", attr_name.replace('-', "_")),
resource_str.span(),
);
if !previous_attrs.insert(snake_name.clone()) {
continue;
}
if attr_name.contains('-') {
Diagnostic::spanned(
resource_span,
Level::Error,
format!("attribute `{attr_name}` contains a '-' character"),
)
.help("replace any '-'s with '_'s")
.emit();
}
let msg = format!(
"Constant referring to Fluent message `{name}.{attr_name}` from `{crate_name}`"
);
constants.extend(quote! {
#[doc = #msg]
pub const #snake_name: rustc_errors::SubdiagMessage =
rustc_errors::SubdiagMessage::FluentAttr(std::borrow::Cow::Borrowed(#attr_name));
});
}
// Record variables referenced by these messages so we can produce
// tests in the derive diagnostics to validate them.
let ident = quote::format_ident!("{snake_name}_refs");
let vrefs = variable_references(msg);
constants.extend(quote! {
#[cfg(test)]
pub const #ident: &[&str] = &[#(#vrefs),*];
})
}
}
for (mref, name) in message_refs.into_iter() {
if !previous_defns.contains_key(mref) {
Diagnostic::spanned(
resource_span,
Level::Error,
format!("referenced message `{mref}` does not exist (in message `{name}`)"),
)
.help(format!("you may have meant to use a variable reference (`{{${mref}}}`)"))
.emit();
}
}
if let Err(errs) = bundle.add_resource(resource) {
for e in errs {
match e {
FluentError::Overriding { kind, id } => {
Diagnostic::spanned(
resource_span,
Level::Error,
format!("overrides existing {kind}: `{id}`"),
)
.emit();
}
FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
}
}
}
finish(constants, quote! { include_str!(#relative_ftl_path) })
}
fn variable_references<'a>(msg: &Message<&'a str>) -> Vec<&'a str> {
let mut refs = vec![];
if let Some(Pattern { elements }) = &msg.value {
for elt in elements {
if let PatternElement::Placeable {
expression: Expression::Inline(InlineExpression::VariableReference { id }),
} = elt
{
refs.push(id.name);
}
}
}
for attr in &msg.attributes {
for elt in &attr.value.elements {
if let PatternElement::Placeable {
expression: Expression::Inline(InlineExpression::VariableReference { id }),
} = elt
{
refs.push(id.name);
}
}
}
refs
}

View file

@ -1,67 +0,0 @@
// tidy-alphabetical-start
#![allow(rustc::default_hash_types)]
#![feature(proc_macro_diagnostic)]
#![feature(proc_macro_tracked_path)]
// tidy-alphabetical-end
use proc_macro::TokenStream;
mod fluent;
/// Implements the `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.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_RESOURCE: &'static [&'static str] = include_str!("./typeck.ftl");
///
/// mod fluent_generated {
/// mod typeck {
/// pub const field_multiply_specified_in_initializer: DiagMessage =
/// DiagMessage::fluent("typeck_field_multiply_specified_in_initializer");
/// pub const field_multiply_specified_in_initializer_label_previous_use: DiagMessage =
/// DiagMessage::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();
/// ```
///
/// Note: any crate using this macro must also have a dependency on
/// `rustc_errors`, because the generated code refers to things from that
/// crate.
#[proc_macro]
pub fn fluent_messages(input: TokenStream) -> TokenStream {
fluent::fluent_messages(input)
}

View file

@ -44,7 +44,6 @@ expression: bench
- Set({bench::compiler/rustc_errors})
- Set({bench::compiler/rustc_expand})
- Set({bench::compiler/rustc_feature})
- Set({bench::compiler/rustc_fluent_macro})
- Set({bench::compiler/rustc_fs_util})
- Set({bench::compiler/rustc_graphviz})
- Set({bench::compiler/rustc_hashes})

View file

@ -26,7 +26,6 @@ expression: build compiler
- Set({build::compiler/rustc_errors})
- Set({build::compiler/rustc_expand})
- Set({build::compiler/rustc_feature})
- Set({build::compiler/rustc_fluent_macro})
- Set({build::compiler/rustc_fs_util})
- Set({build::compiler/rustc_graphviz})
- Set({build::compiler/rustc_hashes})

View file

@ -28,7 +28,6 @@ expression: check
- Set({check::compiler/rustc_errors})
- Set({check::compiler/rustc_expand})
- Set({check::compiler/rustc_feature})
- Set({check::compiler/rustc_fluent_macro})
- Set({check::compiler/rustc_fs_util})
- Set({check::compiler/rustc_graphviz})
- Set({check::compiler/rustc_hashes})

View file

@ -28,7 +28,6 @@ expression: check compiler
- Set({check::compiler/rustc_errors})
- Set({check::compiler/rustc_expand})
- Set({check::compiler/rustc_feature})
- Set({check::compiler/rustc_fluent_macro})
- Set({check::compiler/rustc_fs_util})
- Set({check::compiler/rustc_graphviz})
- Set({check::compiler/rustc_hashes})

View file

@ -28,7 +28,6 @@ expression: check compiletest --include-default-paths
- Set({check::compiler/rustc_errors})
- Set({check::compiler/rustc_expand})
- Set({check::compiler/rustc_feature})
- Set({check::compiler/rustc_fluent_macro})
- Set({check::compiler/rustc_fs_util})
- Set({check::compiler/rustc_graphviz})
- Set({check::compiler/rustc_hashes})

View file

@ -43,7 +43,6 @@ expression: clippy
- Set({clippy::compiler/rustc_errors})
- Set({clippy::compiler/rustc_expand})
- Set({clippy::compiler/rustc_feature})
- Set({clippy::compiler/rustc_fluent_macro})
- Set({clippy::compiler/rustc_fs_util})
- Set({clippy::compiler/rustc_graphviz})
- Set({clippy::compiler/rustc_hashes})

View file

@ -28,7 +28,6 @@ expression: fix
- Set({fix::compiler/rustc_errors})
- Set({fix::compiler/rustc_expand})
- Set({fix::compiler/rustc_feature})
- Set({fix::compiler/rustc_fluent_macro})
- Set({fix::compiler/rustc_fs_util})
- Set({fix::compiler/rustc_graphviz})
- Set({fix::compiler/rustc_hashes})

View file

@ -94,7 +94,6 @@ expression: test
- Set({test::compiler/rustc_errors})
- Set({test::compiler/rustc_expand})
- Set({test::compiler/rustc_feature})
- Set({test::compiler/rustc_fluent_macro})
- Set({test::compiler/rustc_fs_util})
- Set({test::compiler/rustc_graphviz})
- Set({test::compiler/rustc_hashes})

View file

@ -93,7 +93,6 @@ expression: test --skip=coverage
- Set({test::compiler/rustc_errors})
- Set({test::compiler/rustc_expand})
- Set({test::compiler/rustc_feature})
- Set({test::compiler/rustc_fluent_macro})
- Set({test::compiler/rustc_fs_util})
- Set({test::compiler/rustc_graphviz})
- Set({test::compiler/rustc_hashes})

View file

@ -1,6 +1,5 @@
---
source: src/bootstrap/src/core/builder/cli_paths/tests.rs
assertion_line: 68
expression: test --skip=tests
---
[Test] test::Tidy
@ -58,7 +57,6 @@ expression: test --skip=tests
- Set({test::compiler/rustc_errors})
- Set({test::compiler/rustc_expand})
- Set({test::compiler/rustc_feature})
- Set({test::compiler/rustc_fluent_macro})
- Set({test::compiler/rustc_fs_util})
- Set({test::compiler/rustc_graphviz})
- Set({test::compiler/rustc_hashes})

View file

@ -1,6 +1,5 @@
---
source: src/bootstrap/src/core/builder/cli_paths/tests.rs
assertion_line: 68
expression: test --skip=tests --skip=coverage-map --skip=coverage-run --skip=library --skip=tidyselftest
---
[Test] test::Tidy
@ -38,7 +37,6 @@ expression: test --skip=tests --skip=coverage-map --skip=coverage-run --skip=lib
- Set({test::compiler/rustc_errors})
- Set({test::compiler/rustc_expand})
- Set({test::compiler/rustc_feature})
- Set({test::compiler/rustc_fluent_macro})
- Set({test::compiler/rustc_fs_util})
- Set({test::compiler/rustc_graphviz})
- Set({test::compiler/rustc_hashes})

View file

@ -1815,7 +1815,7 @@ mod snapshot {
insta::assert_snapshot!(
ctx.config("check")
.path("compiler")
.render_steps(), @"[check] rustc 0 <host> -> rustc 1 <host> (75 crates)");
.render_steps(), @"[check] rustc 0 <host> -> rustc 1 <host> (74 crates)");
}
#[test]
@ -1841,7 +1841,7 @@ mod snapshot {
ctx.config("check")
.path("compiler")
.stage(1)
.render_steps(), @"[check] rustc 0 <host> -> rustc 1 <host> (75 crates)");
.render_steps(), @"[check] rustc 0 <host> -> rustc 1 <host> (74 crates)");
}
#[test]
@ -1855,7 +1855,7 @@ mod snapshot {
[build] llvm <host>
[build] rustc 0 <host> -> rustc 1 <host>
[build] rustc 1 <host> -> std 1 <host>
[check] rustc 1 <host> -> rustc 2 <host> (75 crates)
[check] rustc 1 <host> -> rustc 2 <host> (74 crates)
");
}
@ -1871,7 +1871,7 @@ mod snapshot {
[build] rustc 0 <host> -> rustc 1 <host>
[build] rustc 1 <host> -> std 1 <host>
[check] rustc 1 <host> -> std 1 <target1>
[check] rustc 1 <host> -> rustc 2 <target1> (75 crates)
[check] rustc 1 <host> -> rustc 2 <target1> (74 crates)
[check] rustc 1 <host> -> rustc 2 <target1>
[check] rustc 1 <host> -> Rustdoc 2 <target1>
[check] rustc 1 <host> -> rustc_codegen_cranelift 2 <target1>
@ -1967,7 +1967,7 @@ mod snapshot {
ctx.config("check")
.paths(&["library", "compiler"])
.args(&args)
.render_steps(), @"[check] rustc 0 <host> -> rustc 1 <host> (75 crates)");
.render_steps(), @"[check] rustc 0 <host> -> rustc 1 <host> (74 crates)");
}
#[test]

View file

@ -3,8 +3,6 @@
/// See <https://github.com/rust-lang/rust/issues/134863>
pub static CRATES: &[&str] = &[
// tidy-alphabetical-start
"annotate-snippets",
"anstyle",
"askama_derive",
"askama_parser",
"basic-toml",
@ -59,7 +57,6 @@ pub static CRATES: &[&str] = &[
"unic-langid-impl",
"unic-langid-macros",
"unicode-ident",
"unicode-width",
"version_check",
"wasm-bindgen-macro-support",
"wasm-bindgen-shared",