提交 19abca11 编写于 作者: B bors

Auto merge of #97476 - Dylan-DPC:rollup-t53nxoe, r=Dylan-DPC

Rollup of 5 pull requests

Successful merges:

 - #94640 (Partially stabilize `(const_)slice_ptr_len` feature by stabilizing `NonNull::len`)
 - #97034 (Implement `Hash` for `core::alloc::Layout`)
 - #97327 (macros: introduce `fluent_messages` macro )
 - #97448 (docs: Don't imply that OsStr on Unix is always UTF-8)
 - #97466 ([bootstrap] Move `sanitize_sh` from `dist` to `install`)

Failed merges:

r? `@ghost`
`@rustbot` modify labels: rollup
......@@ -4010,10 +4010,14 @@ dependencies = [
name = "rustc_macros"
version = "0.1.0"
dependencies = [
"annotate-snippets",
"fluent-bundle",
"fluent-syntax",
"proc-macro2",
"quote",
"syn",
"synstructure",
"unic-langid",
]
[[package]]
......
......@@ -6,7 +6,7 @@
use fluent_bundle::FluentResource;
use fluent_syntax::parser::ParserError;
use rustc_data_structures::sync::Lrc;
use rustc_macros::{Decodable, Encodable};
use rustc_macros::{fluent_messages, Decodable, Encodable};
use rustc_span::Span;
use std::borrow::Cow;
use std::error::Error;
......@@ -29,8 +29,13 @@
pub use fluent_bundle::{FluentArgs, FluentError, FluentValue};
pub use unic_langid::{langid, LanguageIdentifier};
pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] =
&[include_str!("../locales/en-US/typeck.ftl"), include_str!("../locales/en-US/parser.ftl")];
// Generates `DEFAULT_LOCALE_RESOURCES` static and `fluent_generated` module.
fluent_messages! {
parser => "../locales/en-US/parser.ftl",
typeck => "../locales/en-US/typeck.ftl",
}
pub use fluent_generated::{self as fluent, DEFAULT_LOCALE_RESOURCES};
pub type FluentBundle = fluent_bundle::bundle::FluentBundle<FluentResource, IntlLangMemoizer>;
......
......@@ -31,8 +31,8 @@
use rustc_data_structures::sync::{self, Lock, Lrc};
use rustc_data_structures::AtomicRef;
pub use rustc_error_messages::{
fallback_fluent_bundle, fluent_bundle, DiagnosticMessage, FluentBundle, LanguageIdentifier,
LazyFallbackBundle, MultiSpan, SpanLabel, DEFAULT_LOCALE_RESOURCES,
fallback_fluent_bundle, fluent, fluent_bundle, DiagnosticMessage, FluentBundle,
LanguageIdentifier, LazyFallbackBundle, MultiSpan, SpanLabel, DEFAULT_LOCALE_RESOURCES,
};
pub use rustc_lint_defs::{pluralize, Applicability};
use rustc_span::source_map::SourceMap;
......
......@@ -7,7 +7,11 @@ edition = "2021"
proc-macro = true
[dependencies]
annotate-snippets = "0.8.0"
fluent-bundle = "0.15.2"
fluent-syntax = "0.11"
synstructure = "0.12.1"
syn = { version = "1", features = ["full"] }
proc-macro2 = "1"
quote = "1"
unic-langid = { version = "0.9.0", features = ["macros"] }
use annotate_snippets::{
display_list::DisplayList,
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
};
use fluent_bundle::{FluentBundle, FluentError, FluentResource};
use fluent_syntax::{
ast::{Attribute, Entry, Identifier, Message},
parser::ParserError,
};
use proc_macro::{Diagnostic, Level, Span};
use proc_macro2::TokenStream;
use quote::quote;
use std::{
collections::HashMap,
fs::File,
io::Read,
path::{Path, PathBuf},
};
use syn::{
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
token, Ident, LitStr, Result,
};
use unic_langid::langid;
struct Resource {
ident: Ident,
#[allow(dead_code)]
fat_arrow_token: token::FatArrow,
resource: LitStr,
}
impl Parse for Resource {
fn parse(input: ParseStream<'_>) -> Result<Self> {
Ok(Resource {
ident: input.parse()?,
fat_arrow_token: input.parse()?,
resource: input.parse()?,
})
}
}
struct Resources(Punctuated<Resource, token::Comma>);
impl Parse for Resources {
fn parse(input: ParseStream<'_>) -> Result<Self> {
let mut resources = Punctuated::new();
loop {
if input.is_empty() || input.peek(token::Brace) {
break;
}
let value = input.parse()?;
resources.push_value(value);
if !input.peek(token::Comma) {
break;
}
let punct = input.parse()?;
resources.push_punct(punct);
}
Ok(Resources(resources))
}
}
/// 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.source_file().path();
// `/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
}
}
/// See [rustc_macros::fluent_messages].
pub(crate) fn fluent_messages(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let resources = parse_macro_input!(input as Resources);
// 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")]);
// Map of Fluent identifiers to the `Span` of the resource that defined them, used for better
// diagnostics.
let mut previous_defns = HashMap::new();
let mut includes = TokenStream::new();
let mut generated = TokenStream::new();
for res in resources.0 {
let ident_span = res.ident.span().unwrap();
let path_span = res.resource.span().unwrap();
let relative_ftl_path = res.resource.value();
let absolute_ftl_path =
invocation_relative_path_to_absolute(ident_span, &relative_ftl_path);
// As this macro also outputs an `include_str!` for this file, the macro will always be
// re-executed when the file changes.
let mut resource_file = match File::open(absolute_ftl_path) {
Ok(resource_file) => resource_file,
Err(e) => {
Diagnostic::spanned(path_span, Level::Error, "could not open Fluent resource")
.note(e.to_string())
.emit();
continue;
}
};
let mut resource_contents = String::new();
if let Err(e) = resource_file.read_to_string(&mut resource_contents) {
Diagnostic::spanned(path_span, Level::Error, "could not read Fluent resource")
.note(e.to_string())
.emit();
continue;
}
let resource = match FluentResource::try_new(resource_contents) {
Ok(resource) => resource,
Err((this, errs)) => {
Diagnostic::spanned(path_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 line_starts: Vec<usize> = std::iter::once(0)
.chain(
this.source()
.char_indices()
.filter_map(|(i, c)| Some(i + 1).filter(|_| c == '\n')),
)
.collect();
let line_start = line_starts
.iter()
.enumerate()
.map(|(line, idx)| (line + 1, idx))
.filter(|(_, idx)| **idx <= pos.start)
.last()
.unwrap()
.0;
let snippet = Snippet {
title: Some(Annotation {
label: Some(&err),
id: None,
annotation_type: AnnotationType::Error,
}),
footer: vec![],
slices: vec![Slice {
source: this.source(),
line_start,
origin: Some(&relative_ftl_path),
fold: true,
annotations: vec![SourceAnnotation {
label: "",
annotation_type: AnnotationType::Error,
range: (pos.start, pos.end - 1),
}],
}],
opt: Default::default(),
};
let dl = DisplayList::from(snippet);
eprintln!("{}\n", dl);
}
continue;
}
};
let mut constants = TokenStream::new();
for entry in resource.entries() {
let span = res.ident.span();
if let Entry::Message(Message { id: Identifier { name }, attributes, .. }) = entry {
let _ = previous_defns.entry(name.to_string()).or_insert(ident_span);
// `typeck-foo-bar` => `foo_bar`
let snake_name = Ident::new(
&name.replace(&format!("{}-", res.ident), "").replace("-", "_"),
span,
);
constants.extend(quote! {
pub const #snake_name: crate::DiagnosticMessage =
crate::DiagnosticMessage::FluentIdentifier(
std::borrow::Cow::Borrowed(#name),
None
);
});
for Attribute { id: Identifier { name: attr_name }, .. } in attributes {
let attr_snake_name = attr_name.replace("-", "_");
let snake_name = Ident::new(&format!("{snake_name}_{attr_snake_name}"), span);
constants.extend(quote! {
pub const #snake_name: crate::DiagnosticMessage =
crate::DiagnosticMessage::FluentIdentifier(
std::borrow::Cow::Borrowed(#name),
Some(std::borrow::Cow::Borrowed(#attr_name))
);
});
}
}
}
if let Err(errs) = bundle.add_resource(resource) {
for e in errs {
match e {
FluentError::Overriding { kind, id } => {
Diagnostic::spanned(
ident_span,
Level::Error,
format!("overrides existing {}: `{}`", kind, id),
)
.span_help(previous_defns[&id], "previously defined in this resource")
.emit();
}
FluentError::ResolverError(_) | FluentError::ParserError(_) => unreachable!(),
}
}
}
includes.extend(quote! { include_str!(#relative_ftl_path), });
let ident = res.ident;
generated.extend(quote! {
pub mod #ident {
#constants
}
});
}
quote! {
#[allow(non_upper_case_globals)]
#[doc(hidden)]
pub mod fluent_generated {
pub static DEFAULT_LOCALE_RESOURCES: &'static [&'static str] = &[
#includes
];
#generated
}
}
.into()
}
mod diagnostic;
mod error;
mod fluent;
mod subdiagnostic;
mod utils;
use diagnostic::SessionDiagnosticDerive;
pub(crate) use fluent::fluent_messages;
use proc_macro2::TokenStream;
use quote::format_ident;
use subdiagnostic::SessionSubdiagnosticDerive;
......@@ -12,7 +14,7 @@
/// Implements `#[derive(SessionDiagnostic)]`, which allows for errors to be specified as a struct,
/// independent from the actual diagnostics emitting code.
///
/// ```ignore (pseudo-rust)
/// ```ignore (rust)
/// # extern crate rustc_errors;
/// # use rustc_errors::Applicability;
/// # extern crate rustc_span;
......@@ -43,7 +45,7 @@
///
/// Then, later, to emit the error:
///
/// ```ignore (pseudo-rust)
/// ```ignore (rust)
/// sess.emit_err(MoveOutOfBorrowError {
/// expected,
/// actual,
......@@ -67,7 +69,7 @@ pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream {
/// suggestions to be specified as a structs or enums, independent from the actual diagnostics
/// emitting code or diagnostic derives.
///
/// ```ignore (pseudo-rust)
/// ```ignore (rust)
/// #[derive(SessionSubdiagnostic)]
/// pub enum ExpectedIdentifierLabel<'tcx> {
/// #[label(slug = "parser-expected-identifier")]
......@@ -104,7 +106,7 @@ pub fn session_diagnostic_derive(s: Structure<'_>) -> TokenStream {
///
/// Then, later, to add the subdiagnostic:
///
/// ```ignore (pseudo-rust)
/// ```ignore (rust)
/// diag.subdiagnostic(ExpectedIdentifierLabel::WithoutFound { span });
///
/// diag.subdiagnostic(RawIdentifierSuggestion { span, applicability, ident });
......
......@@ -2,6 +2,7 @@
#![feature(let_else)]
#![feature(never_type)]
#![feature(proc_macro_diagnostic)]
#![feature(proc_macro_span)]
#![allow(rustc::default_hash_types)]
#![recursion_limit = "128"]
......@@ -49,6 +50,64 @@ pub fn newtype_index(input: TokenStream) -> TokenStream {
newtype::newtype(input)
}
/// 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 => "./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();
/// ```
#[proc_macro]
pub fn fluent_messages(input: TokenStream) -> TokenStream {
diagnostics::fluent_messages(input)
}
decl_derive!([HashStable, attributes(stable_hasher)] => hash_stable::hash_stable_derive);
decl_derive!(
[HashStable_Generic, attributes(stable_hasher)] =>
......
//! Errors emitted by typeck.
use rustc_errors::{
error_code, Applicability, DiagnosticBuilder, DiagnosticMessage, ErrorGuaranteed,
};
use rustc_errors::{error_code, Applicability, DiagnosticBuilder, ErrorGuaranteed};
use rustc_macros::{SessionDiagnostic, SessionSubdiagnostic};
use rustc_middle::ty::Ty;
use rustc_session::{parse::ParseSess, SessionDiagnostic};
......@@ -264,10 +262,9 @@ pub struct MissingTypeParams {
// Manual implementation of `SessionDiagnostic` to be able to call `span_to_snippet`.
impl<'a> SessionDiagnostic<'a> for MissingTypeParams {
fn into_diagnostic(self, sess: &'a ParseSess) -> DiagnosticBuilder<'a, ErrorGuaranteed> {
static SLUG: &'static str = "typeck-missing-type-params";
let mut err = sess.span_diagnostic.struct_span_err_with_code(
self.span,
DiagnosticMessage::fluent(SLUG),
rustc_errors::fluent::typeck::missing_type_params,
error_code!(E0393),
);
err.set_arg("parameterCount", self.missing_type_params.len());
......@@ -280,7 +277,7 @@ fn into_diagnostic(self, sess: &'a ParseSess) -> DiagnosticBuilder<'a, ErrorGuar
.join(", "),
);
err.span_label(self.def_span, DiagnosticMessage::fluent_attr(SLUG, "label"));
err.span_label(self.def_span, rustc_errors::fluent::typeck::missing_type_params_label);
let mut suggested = false;
if let (Ok(snippet), true) = (
......@@ -298,7 +295,7 @@ fn into_diagnostic(self, sess: &'a ParseSess) -> DiagnosticBuilder<'a, ErrorGuar
// least we can clue them to the correct syntax `Iterator<Type>`.
err.span_suggestion(
self.span,
DiagnosticMessage::fluent_attr(SLUG, "suggestion"),
rustc_errors::fluent::typeck::missing_type_params_suggestion,
format!("{}<{}>", snippet, self.missing_type_params.join(", ")),
Applicability::HasPlaceholders,
);
......@@ -306,10 +303,13 @@ fn into_diagnostic(self, sess: &'a ParseSess) -> DiagnosticBuilder<'a, ErrorGuar
}
}
if !suggested {
err.span_label(self.span, DiagnosticMessage::fluent_attr(SLUG, "no-suggestion-label"));
err.span_label(
self.span,
rustc_errors::fluent::typeck::missing_type_params_no_suggestion_label,
);
}
err.note(DiagnosticMessage::fluent_attr(SLUG, "note"));
err.note(rustc_errors::fluent::typeck::missing_type_params_note);
err
}
}
......
......@@ -26,7 +26,7 @@ const fn size_align<T>() -> (usize, usize) {
/// like this are met, use specific allocators with looser
/// requirements, or use the more lenient `Allocator` interface.)
#[stable(feature = "alloc_layout", since = "1.28.0")]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[lang = "alloc_layout"]
pub struct Layout {
// size of the requested block of memory, measured in bytes.
......
use crate::convert::TryFrom;
use crate::num::NonZeroUsize;
use crate::{cmp, fmt, mem, num};
use crate::{cmp, fmt, hash, mem, num};
/// A type storing a `usize` which is a power of two, and thus
/// represents a possible alignment in the rust abstract machine.
......@@ -105,6 +105,13 @@ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
}
}
impl hash::Hash for ValidAlign {
#[inline]
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.as_nonzero().hash(state)
}
}
#[cfg(target_pointer_width = "16")]
type ValidAlignEnum = ValidAlignEnum16;
#[cfg(target_pointer_width = "32")]
......
......@@ -499,14 +499,15 @@ pub const fn slice_from_raw_parts(data: NonNull<T>, len: usize) -> Self {
/// # Examples
///
/// ```rust
/// #![feature(slice_ptr_len, nonnull_slice_from_raw_parts)]
/// #![feature(nonnull_slice_from_raw_parts)]
/// use std::ptr::NonNull;
///
/// let slice: NonNull<[i8]> = NonNull::slice_from_raw_parts(NonNull::dangling(), 3);
/// assert_eq!(slice.len(), 3);
/// ```
#[unstable(feature = "slice_ptr_len", issue = "71146")]
#[rustc_const_unstable(feature = "const_slice_ptr_len", issue = "71146")]
#[stable(feature = "slice_ptr_len_nonnull", since = "1.63.0")]
#[rustc_const_stable(feature = "const_slice_ptr_len_nonnull", since = "1.63.0")]
#[rustc_allow_const_fn_unstable(const_slice_ptr_len)]
#[must_use]
#[inline]
pub const fn len(self) -> usize {
......
......@@ -104,7 +104,7 @@
//! On Unix, [`OsStr`] implements the
//! <code>std::os::unix::ffi::[OsStrExt][unix.OsStrExt]</code> trait, which
//! augments it with two methods, [`from_bytes`] and [`as_bytes`].
//! These do inexpensive conversions from and to UTF-8 byte slices.
//! These do inexpensive conversions from and to byte slices.
//!
//! Additionally, on Unix [`OsString`] implements the
//! <code>std::os::unix::ffi::[OsStringExt][unix.OsStringExt]</code> trait,
......
......@@ -897,29 +897,6 @@ fn run(self, builder: &Builder<'_>) -> GeneratedTarball {
}
}
// We have to run a few shell scripts, which choke quite a bit on both `\`
// characters and on `C:\` paths, so normalize both of them away.
pub fn sanitize_sh(path: &Path) -> String {
let path = path.to_str().unwrap().replace("\\", "/");
return change_drive(unc_to_lfs(&path)).unwrap_or(path);
fn unc_to_lfs(s: &str) -> &str {
s.strip_prefix("//?/").unwrap_or(s)
}
fn change_drive(s: &str) -> Option<String> {
let mut ch = s.chars();
let drive = ch.next().unwrap_or('C');
if ch.next() != Some(':') {
return None;
}
if ch.next() != Some('/') {
return None;
}
Some(format!("/{}/{}", drive, &s[drive.len_utf8() + 2..]))
}
}
#[derive(Debug, PartialOrd, Ord, Copy, Clone, Hash, PartialEq, Eq)]
pub struct Cargo {
pub compiler: Compiler,
......
......@@ -5,12 +5,12 @@
use std::env;
use std::fs;
use std::path::{Component, PathBuf};
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use crate::util::t;
use crate::dist::{self, sanitize_sh};
use crate::dist;
use crate::tarball::GeneratedTarball;
use crate::Compiler;
......@@ -22,6 +22,29 @@
#[cfg(not(target_os = "illumos"))]
const SHELL: &str = "sh";
// We have to run a few shell scripts, which choke quite a bit on both `\`
// characters and on `C:\` paths, so normalize both of them away.
fn sanitize_sh(path: &Path) -> String {
let path = path.to_str().unwrap().replace("\\", "/");
return change_drive(unc_to_lfs(&path)).unwrap_or(path);
fn unc_to_lfs(s: &str) -> &str {
s.strip_prefix("//?/").unwrap_or(s)
}
fn change_drive(s: &str) -> Option<String> {
let mut ch = s.chars();
let drive = ch.next().unwrap_or('C');
if ch.next() != Some(':') {
return None;
}
if ch.next() != Some('/') {
return None;
}
Some(format!("/{}/{}", drive, &s[drive.len_utf8() + 2..]))
}
}
fn install_sh(
builder: &Builder<'_>,
package: &str,
......
// normalize-stderr-test "note.*" -> "note: os-specific message"
#![feature(rustc_private)]
#![crate_type = "lib"]
extern crate rustc_macros;
use rustc_macros::fluent_messages;
/// Copy of the relevant `DiagnosticMessage` variant constructed by `fluent_messages` as it
/// expects `crate::DiagnosticMessage` to exist.
pub enum DiagnosticMessage {
FluentIdentifier(std::borrow::Cow<'static, str>, Option<std::borrow::Cow<'static, str>>),
}
mod missing_absolute {
use super::fluent_messages;
fluent_messages! {
missing_absolute => "/definitely_does_not_exist.ftl",
//~^ ERROR could not open Fluent resource
}
}
mod missing_relative {
use super::fluent_messages;
fluent_messages! {
missing_relative => "../definitely_does_not_exist.ftl",
//~^ ERROR could not open Fluent resource
}
}
mod missing_message {
use super::fluent_messages;
fluent_messages! {
missing_message => "./missing-message.ftl",
//~^ ERROR could not parse Fluent resource
}
}
mod duplicate {
use super::fluent_messages;
fluent_messages! {
a => "./duplicate-a.ftl",
b => "./duplicate-b.ftl",
//~^ ERROR overrides existing message: `key`
}
}
mod valid {
use super::fluent_messages;
fluent_messages! {
valid => "./valid.ftl",
}
use self::fluent_generated::{DEFAULT_LOCALE_RESOURCES, valid::valid};
}
error: could not open Fluent resource
--> $DIR/test.rs:19:29
|
LL | missing_absolute => "/definitely_does_not_exist.ftl",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: os-specific message
error: could not open Fluent resource
--> $DIR/test.rs:28:29
|
LL | missing_relative => "../definitely_does_not_exist.ftl",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: os-specific message
error: could not parse Fluent resource
--> $DIR/test.rs:37:28
|
LL | missing_message => "./missing-message.ftl",
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= help: see additional errors emitted
error: expected a message field for "missing-message"
--> ./missing-message.ftl:1:1
|
1 | missing-message =
| ^^^^^^^^^^^^^^^^^^
|
error: overrides existing message: `key`
--> $DIR/test.rs:47:9
|
LL | b => "./duplicate-b.ftl",
| ^
|
help: previously defined in this resource
--> $DIR/test.rs:46:9
|
LL | a => "./duplicate-a.ftl",
| ^
error: aborting due to 4 previous errors
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册