diff --git a/kclvm/compiler_base/error/Cargo.toml b/kclvm/compiler_base/error/Cargo.toml index 62b678e79..f7ec91b00 100644 --- a/kclvm/compiler_base/error/Cargo.toml +++ b/kclvm/compiler_base/error/Cargo.toml @@ -8,4 +8,9 @@ edition = "2021" [dependencies] compiler_base_macros = {path = "../macros", version = "0.1.0"} rustc_errors = {path="../3rdparty/rustc_errors", version="0.1.0"} -termcolor = "1.0" \ No newline at end of file +unic-langid = {version="0.9.0", features = ["macros"]} + +fluent = "0.16.0" +termcolor = "1.0" +walkdir = "2" +anyhow = "1.0" \ No newline at end of file diff --git a/kclvm/compiler_base/error/src/diagnostic/diagnostic_message.rs b/kclvm/compiler_base/error/src/diagnostic/diagnostic_message.rs new file mode 100644 index 000000000..5f00ad0f4 --- /dev/null +++ b/kclvm/compiler_base/error/src/diagnostic/diagnostic_message.rs @@ -0,0 +1,239 @@ +//! The crate provides `TemplateLoader` to load the diagnositc message displayed in diagnostics from "*.ftl" files, +//! +use anyhow::{bail, Context, Result}; +use fluent::{FluentArgs, FluentBundle, FluentResource}; +use std::{fs, sync::Arc}; +use unic_langid::langid; +use walkdir::{DirEntry, WalkDir}; + +/// Struct `TemplateLoader` load template contents from "*.ftl" file. +/// +/// `TemplateLoader` will operate on files locally. +/// +/// In order to avoid the performance loss and thread safety problems that +/// may occur during the constructing the `TemplateLoader`, we close the constructor of `TemplateLoader`. +/// +/// You only need to pass the path of the "*.ftl" file to `DiagnosticHandler`, +/// and `DiagnosticHandler` will automatically construct `TemplateLoader` and load the template file. +/// +/// `TemplateLoader` is only useful for you, when you want to get message from template file by `get_msg_to_str()`. +/// For more information about how to use `get_msg_to_str()`, see the doc above `get_msg_to_str()`. +pub struct TemplateLoader { + template_inner: Arc, +} + +impl TemplateLoader { + // Create the `TemplateLoader` with template (*.ftl) files directory. + // `TemplateLoader` will load all the files end with "*.ftl" under the directory recursively. + // + // template_files + // | + // |---- template.ftl + // |---- sub_template_files + // | + // |---- sub_template.ftl + // + // 'template.ftl' and 'sub_template.ftl' can both loaded by the `new_with_template_dir()`. + pub(crate) fn new_with_template_dir(template_dir: &str) -> Result { + let template_inner = TemplateLoaderInner::new_with_template_dir(template_dir) + .with_context(|| format!("Failed to load '*.ftl' from '{}'", template_dir))?; + Ok(Self { + template_inner: Arc::new(template_inner), + }) + } + + /// Get the message string from "*.ftl" file by `index`, `sub_index` and `MessageArgs`. + /// For more information about "*.ftl" file, see the doc above `TemplateLoader`. + /// + /// "*.ftl" file looks like, e.g. './src/diagnostic/locales/en-US/default.ftl' : + /// + /// ``` ignore + /// 1. invalid-syntax = Invalid syntax + /// 2. .expected = Expected one of `{$expected_items}` + /// ``` + /// + /// - In line 1, `invalid-syntax` is a `index`, `Invalid syntax` is the `Message String` to this `index`. + /// - In line 2, `.expected` is another `index`, it is a `sub_index` of `invalid-syntax`. + /// - In line 2, `sub_index` must start with a point `.` and it is optional. + /// - In line 2, `Expected one of `{$expected_items}`` is the `Message String` to `.expected`. It is an interpolated string. + /// - In line 2, `{$expected_items}` is a `MessageArgs` of the `Expected one of `{$expected_items}`` + /// and `MessageArgs` can be recognized as a Key-Value entry, it is optional. + /// + /// The pattern of above '*.ftl' file looks like: + /// ``` ignore + /// 1. <'index'> = <'message_string' with optional 'MessageArgs'> + /// 2. = <'message_string' with optional 'MessageArgs'> + /// ``` + /// And for the 'default.ftl' shown above, you can get messages as follow: + /// + /// 1. If you want the message 'Invalid syntax' in line 1. + /// + /// ```ignore rust + /// # use compiler_base_error::diagnostic::diagnostic_message::TemplateLoader; + /// # use compiler_base_error::diagnostic::diagnostic_message::MessageArgs; + /// # use std::borrow::Borrow; + /// + /// // 1. Prepare an empty `MessageArgs`, Message in line 1 is not an interpolated string. + /// let no_args = MessageArgs::new(); + /// + /// // 2. `index` is 'invalid-syntax' and has no `sub_index`. + /// let index = "invalid-syntax"; + /// let sub_index = None; + /// + /// // 3. Create the `TemplateLoader` with template (*.ftl) files directory. + /// // We cloesd the constructor of `TemplateLoader`. + /// // For more information, see the doc above the `TemplateLoader`. + /// let error_message = TemplateLoader::new_with_template_dir("./src/diagnostic/locales/en-US/").unwrap(); + /// let msg_in_line_1 = error_message.get_msg_to_str(index, sub_index, &no_args).unwrap(); + /// + /// assert_eq!(msg_in_line_1, "Invalid syntax"); + /// ``` + /// + /// 2. If you want the message 'Expected one of `{$expected_items}`' in line 2. + /// + /// ```ignore rust + /// # use compiler_base_error::diagnostic::diagnostic_message::TemplateLoader; + /// # use compiler_base_error::diagnostic::diagnostic_message::MessageArgs; + /// # use std::borrow::Borrow; + /// + /// // 1. Prepare the `MessageArgs` for `{$expected_items}`. + /// let mut args = MessageArgs::new(); + /// args.set("expected_items", "I am an expected item"); + /// + /// // 2. `index` is 'invalid-syntax'. + /// let index = "invalid-syntax"; + /// + /// // 3. `sub_index` is 'expected'. + /// let sub_index = "expected"; + /// + /// // 4. With the help of `TemplateLoader`, you can get the message in 'default.ftl'. + /// // We cloesd the constructor of `TemplateLoader`. + /// // For more information, see the doc above the `TemplateLoader`. + /// let error_message = TemplateLoader::new_with_template_dir("./src/diagnostic/locales/en-US/").unwrap(); + /// let msg_in_line_2 = error_message.get_msg_to_str(index, Some(sub_index), &args).unwrap(); + /// + /// assert_eq!(msg_in_line_2, "Expected one of `\u{2068}I am an expected item\u{2069}`"); + /// ``` + pub fn get_msg_to_str( + &self, + index: &str, + sub_index: Option<&str>, + args: &MessageArgs, + ) -> Result { + let msg = match self.template_inner.get_template_bunder().get_message(index) { + Some(m) => m, + None => bail!("Message doesn't exist."), + }; + + let pattern = match sub_index { + Some(s_id) => { + let attr = msg.get_attribute(s_id).unwrap(); + attr.value() + } + None => match msg.value() { + Some(v) => v, + None => bail!("Message has no value."), + }, + }; + + let MessageArgs(args) = args; + let value = self.template_inner.get_template_bunder().format_pattern( + pattern, + Some(&args), + &mut vec![], + ); + Ok(value.to_string()) + } +} + +/// `MessageArgs` is the arguments of the interpolated string. +/// +/// `MessageArgs` is a Key-Value entry which only supports "set" and without "get". +/// You need getting nothing from `MessageArgs`. Only setting it and senting it to `TemplateLoader` is enough. +/// +/// Note: Currently both `Key` and `Value` of `MessageArgs` types only support string (&str). +/// +/// # Examples +/// +/// ```ignore rust +/// # use compiler_base_error::diagnostic::diagnostic_message::MessageArgs; +/// # use compiler_base_error::diagnostic::diagnostic_message::TemplateLoader; +/// # use std::borrow::Borrow; +/// +/// let index = "invalid-syntax"; +/// let sub_index = Some("expected"); +/// let mut msg_args = MessageArgs::new(); +/// // You only need "set()". +/// msg_args.set("This is Key", "This is Value"); +/// +/// // We cloesd the constructor of `TemplateLoader`. +/// // For more information, see the doc above the `TemplateLoader`. +/// let error_message = TemplateLoader::new_with_template_dir("./src/diagnostic/locales/en-US/").unwrap(); +/// +/// // When you use it, just sent it to `TemplateLoader`. +/// let msg_in_line_1 = error_message.get_msg_to_str(index, sub_index, &msg_args); +/// ``` +/// +/// For more information about the `TemplateLoader` see the doc above struct `TemplateLoader`. +pub struct MessageArgs<'a>(FluentArgs<'a>); +impl<'a> MessageArgs<'a> { + pub fn new() -> Self { + Self(FluentArgs::new()) + } + + pub fn set(&mut self, k: &'a str, v: &'a str) { + self.0.set(k, v); + } +} + +// `TemplateLoaderInner` is used to privatize the default constructor of `TemplateLoader`. +struct TemplateLoaderInner { + template_bunder: FluentBundle, +} + +impl TemplateLoaderInner { + fn new_with_template_dir(template_dir: &str) -> Result { + let mut template_bunder = FluentBundle::new(vec![langid!("en-US")]); + load_all_templates_in_dir_to_resources(template_dir, &mut template_bunder) + .with_context(|| format!("Failed to load '*.ftl' from '{}'", template_dir))?; + Ok(Self { template_bunder }) + } + + fn get_template_bunder(&self) -> &FluentBundle { + &self.template_bunder + } +} + +fn is_ftl_file(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|s| s.ends_with(".ftl")) + .unwrap_or(false) +} + +fn load_all_templates_in_dir_to_resources( + dir: &str, + fluent_bundle: &mut FluentBundle, +) -> Result<()> { + if !std::path::Path::new(&dir).exists() { + bail!("Failed to load '*.ftl' dir"); + } + + for entry in WalkDir::new(dir) { + let entry = entry?; + + if is_ftl_file(&entry) { + let resource = fs::read_to_string(entry.path())?; + + match FluentResource::try_new(resource) { + Ok(s) => match fluent_bundle.add_resource(s) { + Err(_) => bail!("Failed to parse an FTL string."), + Ok(_) => {} + }, + Err(_) => bail!("Failed to add FTL resources to the bundle."), + }; + } + } + Ok(()) +} diff --git a/kclvm/compiler_base/error/src/diagnostic/locales/en-US/default.ftl b/kclvm/compiler_base/error/src/diagnostic/locales/en-US/default.ftl new file mode 100644 index 000000000..67e493db8 --- /dev/null +++ b/kclvm/compiler_base/error/src/diagnostic/locales/en-US/default.ftl @@ -0,0 +1,3 @@ +invalid-syntax = + Invalid syntax + .expected = Expected one of `{$expected_items}` diff --git a/kclvm/compiler_base/error/src/diagnostic/locales/en-US/test/default1.ftl b/kclvm/compiler_base/error/src/diagnostic/locales/en-US/test/default1.ftl new file mode 100644 index 000000000..4290d805b --- /dev/null +++ b/kclvm/compiler_base/error/src/diagnostic/locales/en-US/test/default1.ftl @@ -0,0 +1,3 @@ +invalid-syntax-1 = + Invalid syntax 1 + .expected_1 = Expected one of `{$expected_items}` 1 diff --git a/kclvm/compiler_base/error/src/diagnostic/mod.rs b/kclvm/compiler_base/error/src/diagnostic/mod.rs index 316cb299a..e7c11f5a9 100644 --- a/kclvm/compiler_base/error/src/diagnostic/mod.rs +++ b/kclvm/compiler_base/error/src/diagnostic/mod.rs @@ -1,5 +1,6 @@ pub use rustc_errors::styled_buffer::StyledBuffer; use rustc_errors::Style; +pub mod diagnostic_message; pub mod components; pub mod style; @@ -21,6 +22,9 @@ where /// # Examples /// /// ```rust + /// # use compiler_base_error::diagnostic::style::DiagnosticStyle; + /// # use compiler_base_error::diagnostic::StyledBuffer; + /// # use compiler_base_error::diagnostic::Component; /// struct ComponentWithStyleLogo { /// text: String /// } diff --git a/kclvm/compiler_base/error/src/diagnostic/style.rs b/kclvm/compiler_base/error/src/diagnostic/style.rs index 7db6e003f..fca8e8985 100644 --- a/kclvm/compiler_base/error/src/diagnostic/style.rs +++ b/kclvm/compiler_base/error/src/diagnostic/style.rs @@ -55,7 +55,7 @@ impl DiagnosticStyle { /// /// ```rust /// # use rustc_errors::Style; - /// # use compiler_base_error::style::DiagnosticStyle; + /// # use compiler_base_error::diagnostic::style::DiagnosticStyle; /// /// let mut color_spec = DiagnosticStyle::NeedFix.render_style_to_color_spec(); /// assert!(DiagnosticStyle::NeedFix.check_is_expected_colorspec(&color_spec)); diff --git a/kclvm/compiler_base/error/src/diagnostic/tests.rs b/kclvm/compiler_base/error/src/diagnostic/tests.rs index 722c9b28f..a651d1f54 100644 --- a/kclvm/compiler_base/error/src/diagnostic/tests.rs +++ b/kclvm/compiler_base/error/src/diagnostic/tests.rs @@ -75,3 +75,51 @@ mod test_components { assert_eq!(result.get(0).unwrap().get(0).unwrap().style, None); } } + +mod test_error_message { + use crate::diagnostic::diagnostic_message::{MessageArgs, TemplateLoader}; + + #[test] + fn test_template_message() { + let template_dir = "./src/diagnostic/locales/en-US"; + let template_loader = TemplateLoader::new_with_template_dir(template_dir).unwrap(); + + let mut args = MessageArgs::new(); + check_template_msg( + "invalid-syntax", + None, + &args, + "Invalid syntax", + &template_loader, + ); + + args.set("expected_items", "I am an expected item"); + check_template_msg( + "invalid-syntax", + Some("expected"), + &args, + "Expected one of `\u{2068}I am an expected item\u{2069}`", + &template_loader, + ); + + args.set("expected_items", "I am an expected item"); + check_template_msg( + "invalid-syntax-1", + Some("expected_1"), + &args, + "Expected one of `\u{2068}I am an expected item\u{2069}` 1", + &template_loader, + ); + } + + fn check_template_msg( + index: &str, + sub_index: Option<&str>, + args: &MessageArgs, + expected_msg: &str, + template_loader: &TemplateLoader, + ) { + let msg_in_line = template_loader.get_msg_to_str(index, sub_index, &args); + assert_eq!(msg_in_line.unwrap(), expected_msg); + } +} diff --git a/kclvm/compiler_base/error/src/emitter.rs b/kclvm/compiler_base/error/src/emitter.rs index e3d7f79b6..148d80388 100644 --- a/kclvm/compiler_base/error/src/emitter.rs +++ b/kclvm/compiler_base/error/src/emitter.rs @@ -30,7 +30,7 @@ use termcolor::{Buffer, BufferWriter, ColorChoice, ColorSpec, StandardStream, Wr /// /// 1. Define your Emitter: /// -/// ```no_run rust +/// ```ignore rust /// /// // create a new `Emitter` /// struct DummyEmitter { @@ -64,7 +64,7 @@ use termcolor::{Buffer, BufferWriter, ColorChoice, ColorSpec, StandardStream, Wr /// /// 2. Use your Emitter with diagnostic: /// -/// ```no_run rust +/// ```ignore rust /// /// // Create a diagnostic for emitting. /// let mut diagnostic = Diagnostic::::new();