Skip to content

feat: Translatable diagnostics #19545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ debug = 2
# local crates
base-db = { path = "./crates/base-db", version = "0.0.0" }
cfg = { path = "./crates/cfg", version = "0.0.0", features = ["tt"] }
fluent-build = { path = "./crates/fluent-build", version = "0.0.0" }
fluent-files = { path = "./crates/fluent-files", version = "0.0.0" }
hir = { path = "./crates/hir", version = "0.0.0" }
hir-def = { path = "./crates/hir-def", version = "0.0.0" }
hir-expand = { path = "./crates/hir-expand", version = "0.0.0" }
Expand Down Expand Up @@ -113,6 +115,7 @@ dissimilar = "1.0.10"
dot = "0.1.4"
either = "1.15.0"
expect-test = "1.5.1"
fluent-syntax = "0.11.1"
indexmap = { version = "2.8.0", features = ["serde"] }
itertools = "0.14.0"
libc = "0.2.171"
Expand All @@ -127,9 +130,11 @@ object = { version = "0.36.7", default-features = false, features = [
"macho",
"pe",
] }
proc-macro2 = "1"
process-wrap = { version = "8.2.0", features = ["std"] }
pulldown-cmark-to-cmark = "10.0.4"
pulldown-cmark = { version = "0.9.6", default-features = false }
quote = "1"
rayon = "1.10.0"
salsa = "0.19"
rustc-hash = "2.1.1"
Expand Down
30 changes: 30 additions & 0 deletions crates/fluent-build/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "fluent-build"
version = "0.0.0"
repository.workspace = true
description = "Utils for build-time support for fluent files"

authors.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true

[lib]

[dependencies]
fluent-syntax.workspace = true
proc-macro2.workspace = true
quote.workspace = true

# local deps

[dev-dependencies]
expect-test = "1.5.1"

# local deps
test-utils.workspace = true
test-fixture.workspace = true

[lints]
workspace = true

24 changes: 24 additions & 0 deletions crates/fluent-build/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::io;

pub type FluentParserError =
(fluent_syntax::ast::Resource<String>, Vec<fluent_syntax::parser::ParserError>);

#[derive(Debug)]
pub enum FluentBuildError {
Io(io::Error),
Parser(FluentParserError),
/// Panic errors
Custom(String),
}

impl From<io::Error> for FluentBuildError {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}

impl From<FluentParserError> for FluentBuildError {
fn from(value: FluentParserError) -> Self {
Self::Parser(value)
}
}
189 changes: 189 additions & 0 deletions crates/fluent-build/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
pub mod error;

use std::path::PathBuf;
use std::{env, fs, io};

use error::FluentBuildError;
use fluent_syntax::ast;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

fn get_cargo_manifest() -> String {
env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| panic!("Running on non cargo environment"))
}

pub fn build_fluent_src() -> io::Result<()> {
let mut output = "//! This file is generated by `fluent-build`".to_owned();

for entry in fs::read_dir("src")? {
let entry = entry?.path();

if !entry.extension().is_none_or(|e| e.to_string_lossy().to_string() == "ftl") {
continue;
}

let output_mod = entry
.file_stem()
.unwrap()
.to_string_lossy()
.to_string()
.replace("-", "_")
.replace(".", "_");
let output_file =
entry.with_extension("rs").file_name().unwrap().to_string_lossy().to_string();

output +=
&format!("\n#[path = \"{output_file}\"]\nmod {output_mod};\npub use {output_mod}::*;");

build_fluent_file(entry).map_err(|e| match e {
FluentBuildError::Io(error) => return error,
FluentBuildError::Parser(_) => todo!("parse error"),
FluentBuildError::Custom(c) => panic!("{c}"),
})?;
}

let absolute_path = PathBuf::from(get_cargo_manifest()).join("src/generated.rs");
fs::write(absolute_path, output)?;

Ok(())
}

pub fn build_fluent_file(relative_path: PathBuf) -> Result<(), FluentBuildError> {
let absolute_path = PathBuf::from(get_cargo_manifest()).join(relative_path);
let resource = fs::read_to_string(&absolute_path)
.map_err(|e| FluentBuildError::Custom(format!("{absolute_path:#?}: {e}")))?;

for esc in ["\\n", "\\\"", "\\'"] {
if resource.contains(esc) {
return Err(FluentBuildError::Custom(format!(
"invalid escape `{esc}` in Fluent resource.\nFluent does not interpret these escape sequences (<https://projectfluent.org/fluent/guide/special.html>)"
)));
}
}

// Use parse_runtime because we don't need debug info
let resource = fluent_syntax::parser::parse_runtime(resource)?.body;

let entries = resource
.into_iter()
.filter_map(|entry| {
let (name, value, attributes, comment) = match entry {
ast::Entry::Message(ast::Message {
id,
value: Some(value),
attributes,
comment,
}) => (id.name, value, attributes, comment),
ast::Entry::Term(ast::Term { id, value, attributes, comment }) => {
(id.name, value, attributes, comment)
}
_ => return None,
};

let children = attributes
.into_iter()
.map(|ast::Attribute { id, value }| {
entry_to_code(format!("{name}-{}", id.name), value, None)
})
.collect::<Vec<_>>();

let mut code = entry_to_code(name, value, comment).to_string();

for child in children {
code.push('\n');
code += &child.to_string();
}

Some(code)
})
.collect::<Vec<_>>();

let code = entries.join("\n");
let result_file = absolute_path.with_extension("rs");

fs::write(result_file, code)?;

Ok(())
}

fn entry_to_code(
name: String,
pattern: ast::Pattern<String>,
comment: Option<ast::Comment<String>>,
) -> TokenStream {
let name = format_ident!("{}", name.replace("-", "_"));

let is_dynamic =
pattern.elements.iter().any(|elm| matches!(elm, ast::PatternElement::Placeable { .. }));

let comment = comment
.map(|comment| {
let content = comment.content.into_iter().map(|c| quote! {#[doc = #c]});
quote! {#(#content)*}
})
.unwrap_or(quote! {});

if is_dynamic {
let (args, format_str) = pattern.elements.into_iter().fold(
(Vec::new(), String::new()),
|(mut args, mut body), elm| {
resolve_pattern_element(&mut args, &mut body, elm);
(args, body)
},
);

quote! {
#comment
pub fn #name(#(#args),*) -> String {
format!(#format_str)
}
}
} else {
// Collect all static elements
let body = pattern
.elements
.into_iter()
.filter_map(|elm| match elm {
ast::PatternElement::TextElement { value } => Some(value),
_ => None,
})
.collect::<String>();

quote! {
#comment
#[allow(non_upper_case_globals)]
pub const #name: &str = #body;
}
}
}

fn resolve_pattern_element(
args: &mut Vec<TokenStream>,
body: &mut String,
elm: ast::PatternElement<String>,
) {
match elm {
ast::PatternElement::TextElement { value } => {
*body += &value;
}
ast::PatternElement::Placeable { expression } => match expression {
ast::Expression::Select { .. } => todo!(),
ast::Expression::Inline(inline_expression) => match inline_expression {
ast::InlineExpression::StringLiteral { .. } => todo!(),
ast::InlineExpression::NumberLiteral { .. } => todo!(),
ast::InlineExpression::FunctionReference { .. } => todo!(),
ast::InlineExpression::MessageReference { .. } => todo!(),
ast::InlineExpression::TermReference { .. } => todo!(),
ast::InlineExpression::VariableReference { id } => {
let arg = format_ident!("{}", id.name);
args.push(quote! { #arg : impl ::std::fmt::Display });

body.push('{');
body.push_str(&id.name);
body.push('}');
}
ast::InlineExpression::Placeable { .. } => todo!("Expression inception"),
},
},
}
}
2 changes: 2 additions & 0 deletions crates/fluent-files/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
src/*.rs
!src/lib.rs
28 changes: 28 additions & 0 deletions crates/fluent-files/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "fluent-files"
version = "0.0.0"
repository.workspace = true
description = "Collection of translated files"

authors.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true

[lib]

[build-dependencies]
# local deps
fluent-build.workspace = true

[dev-dependencies]
expect-test = "1.5.1"

# local deps
test-utils.workspace = true
test-fixture.workspace = true

[lints]
workspace = true


7 changes: 7 additions & 0 deletions crates/fluent-files/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fn main() {
println!("cargo::rerun-if-changed=src/");
let result = fluent_build::build_fluent_src();
if let Err(r) = result {
panic!("{r:#?}")
}
}
2 changes: 2 additions & 0 deletions crates/fluent-files/src/await_outside_of_async.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
await-outside-of-async-code = E0728
await-outside-of-async = `await` is used inside {$ctx}, which is not an `async` context
2 changes: 2 additions & 0 deletions crates/fluent-files/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod generated;
pub use generated::*;
1 change: 1 addition & 0 deletions crates/ide-diagnostics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ serde_json.workspace = true
tracing.workspace = true

# local deps
fluent-files.workspace = true
stdx.workspace = true
syntax.workspace = true
cfg.workspace = true
Expand Down
4 changes: 2 additions & 2 deletions crates/ide-diagnostics/src/handlers/await_outside_of_async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pub(crate) fn await_outside_of_async(
let display_range =
adjusted_display_range(ctx, d.node, &|node| Some(node.await_token()?.text_range()));
Diagnostic::new(
crate::DiagnosticCode::RustcHardError("E0728"),
format!("`await` is used inside {}, which is not an `async` context", d.location),
crate::DiagnosticCode::RustcHardError(fluent_files::await_outside_of_async_code),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rustc errors do not change with translations

fluent_files::await_outside_of_async(&d.location),
display_range,
)
}
Expand Down
Loading