From 08d1e92c07b4936674af25d56b22a22b6a3075f3 Mon Sep 17 00:00:00 2001 From: maciektr Date: Tue, 12 Mar 2024 10:04:59 +0100 Subject: [PATCH] Implement procedural macro diagnostics (#1159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit-id:f06e9653 --- **Stack**: - #1166 - #1165 - #1161 - #1159 ⬅ ⚠️ *Part of a stack created by [spr](https://github.com/ejoffe/spr). Do not merge manually using the UI - doing so may have unexpected results.* --- Cargo.lock | 1 - .../cairo-lang-macro-attributes/Cargo.toml | 1 - .../cairo-lang-macro-attributes/src/lib.rs | 10 +- plugins/cairo-lang-macro-stable/src/ffi.rs | 37 +++ plugins/cairo-lang-macro-stable/src/lib.rs | 40 ++- plugins/cairo-lang-macro/src/lib.rs | 285 ++++++++++++++++-- scarb/src/compiler/plugin/proc_macro/ffi.rs | 12 +- scarb/src/compiler/plugin/proc_macro/host.rs | 46 ++- scarb/tests/build_cairo_plugin.rs | 137 +++++++-- 9 files changed, 501 insertions(+), 68 deletions(-) create mode 100644 plugins/cairo-lang-macro-stable/src/ffi.rs diff --git a/Cargo.lock b/Cargo.lock index f61762d52..1bc3c9e3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -741,7 +741,6 @@ dependencies = [ name = "cairo-lang-macro-attributes" version = "0.0.1" dependencies = [ - "camino", "quote", "syn 2.0.52", ] diff --git a/plugins/cairo-lang-macro-attributes/Cargo.toml b/plugins/cairo-lang-macro-attributes/Cargo.toml index 1d3787117..cd5b4a640 100644 --- a/plugins/cairo-lang-macro-attributes/Cargo.toml +++ b/plugins/cairo-lang-macro-attributes/Cargo.toml @@ -16,6 +16,5 @@ repository.workspace = true proc-macro = true [dependencies] -camino.workspace = true quote.workspace = true syn = { workspace = true, features = ["full", "extra-traits"] } diff --git a/plugins/cairo-lang-macro-attributes/src/lib.rs b/plugins/cairo-lang-macro-attributes/src/lib.rs index e39294732..1347361b7 100644 --- a/plugins/cairo-lang-macro-attributes/src/lib.rs +++ b/plugins/cairo-lang-macro-attributes/src/lib.rs @@ -16,10 +16,14 @@ pub fn attribute_macro(_args: TokenStream, input: TokenStream) -> TokenStream { #item #[no_mangle] - pub unsafe extern "C" fn expand(token_stream: cairo_lang_macro_stable::StableTokenStream) -> cairo_lang_macro_stable::StableProcMacroResult { - let token_stream = cairo_lang_macro::TokenStream::from_stable(token_stream); + pub unsafe extern "C" fn expand(stable_token_stream: cairo_lang_macro_stable::StableTokenStream) -> cairo_lang_macro_stable::StableResultWrapper { + let token_stream = cairo_lang_macro::TokenStream::from_stable(&stable_token_stream); let result = #item_name(token_stream); - result.into_stable() + let result: cairo_lang_macro_stable::StableProcMacroResult = result.into_stable(); + cairo_lang_macro_stable::StableResultWrapper { + input: stable_token_stream, + output: result, + } } }; TokenStream::from(expanded) diff --git a/plugins/cairo-lang-macro-stable/src/ffi.rs b/plugins/cairo-lang-macro-stable/src/ffi.rs new file mode 100644 index 000000000..a0ecbe4ce --- /dev/null +++ b/plugins/cairo-lang-macro-stable/src/ffi.rs @@ -0,0 +1,37 @@ +/// This struct encapsulates a stable ABI representation of information required to construct a slice. +/// +/// Please be aware that the memory management of values passing the FFI-barrier is tricky. +/// The memory must be freed on the same side of the barrier, where the allocation was made. +#[repr(C)] +#[derive(Debug)] +#[doc(hidden)] +pub struct StableSlice { + ptr: *mut T, + len: usize, +} + +impl StableSlice { + /// Create a new `StableSlice` from a Vector. + /// Note that the vector will not be deallocated automatically. + /// Please make sure to use `into_owned` afterward, to free the memory. + pub fn new(mut x: Vec) -> Self { + x.shrink_to_fit(); + assert_eq!(x.len(), x.capacity()); + let ptr = x.as_mut_ptr(); + let len = x.len(); + std::mem::forget(x); + Self { ptr, len } + } + + /// Convert to owned vector. + pub fn into_owned(self) -> Vec { + unsafe { Vec::from_raw_parts(self.ptr, self.len, self.len) } + } + + /// Returns raw pointer and length. + /// Can be used to construct a slice. + /// No ownership is transferred. + pub fn raw_parts(&self) -> (*mut T, usize) { + (self.ptr, self.len) + } +} diff --git a/plugins/cairo-lang-macro-stable/src/lib.rs b/plugins/cairo-lang-macro-stable/src/lib.rs index a431094a0..fcf853b96 100644 --- a/plugins/cairo-lang-macro-stable/src/lib.rs +++ b/plugins/cairo-lang-macro-stable/src/lib.rs @@ -1,35 +1,65 @@ +use crate::ffi::StableSlice; use std::ffi::{CStr, CString}; +use std::num::NonZeroU8; use std::os::raw::c_char; +pub mod ffi; + /// Token stream. /// /// This struct implements FFI-safe stable ABI. #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct StableTokenStream(*mut c_char); #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum StableAuxData { None, Some(*mut c_char), } +/// Diagnostic returned by the procedural macro. +/// +/// This struct implements FFI-safe stable ABI. +#[repr(C)] +#[derive(Debug)] +pub struct StableDiagnostic { + pub message: *mut c_char, + pub severity: StableSeverity, +} + +/// The severity of a diagnostic. +/// +/// This struct implements FFI-safe stable ABI. +pub type StableSeverity = NonZeroU8; + /// Procedural macro result. /// /// This struct implements FFI-safe stable ABI. #[repr(C)] -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum StableProcMacroResult { /// Plugin has not taken any action. - Leave, + Leave { + diagnostics: StableSlice, + }, /// Plugin generated [`StableTokenStream`] replacement. Replace { + diagnostics: StableSlice, token_stream: StableTokenStream, aux_data: StableAuxData, }, /// Plugin ordered item removal. - Remove, + Remove { + diagnostics: StableSlice, + }, +} + +#[repr(C)] +pub struct StableResultWrapper { + pub input: StableTokenStream, + pub output: StableProcMacroResult, } impl StableTokenStream { diff --git a/plugins/cairo-lang-macro/src/lib.rs b/plugins/cairo-lang-macro/src/lib.rs index 735f37504..6613c7892 100644 --- a/plugins/cairo-lang-macro/src/lib.rs +++ b/plugins/cairo-lang-macro/src/lib.rs @@ -1,20 +1,26 @@ +pub use cairo_lang_macro_attributes::*; +use cairo_lang_macro_stable::ffi::StableSlice; +use cairo_lang_macro_stable::{ + StableAuxData, StableDiagnostic, StableProcMacroResult, StableSeverity, StableTokenStream, +}; use std::ffi::{c_char, CStr, CString}; use std::fmt::Display; - -pub use cairo_lang_macro_attributes::*; -use cairo_lang_macro_stable::{StableAuxData, StableProcMacroResult, StableTokenStream}; +use std::num::NonZeroU8; +use std::slice; +use std::vec::IntoIter; #[derive(Debug)] pub enum ProcMacroResult { /// Plugin has not taken any action. - Leave, + Leave { diagnostics: Vec }, /// Plugin generated [`TokenStream`] replacement. Replace { token_stream: TokenStream, aux_data: Option, + diagnostics: Vec, }, /// Plugin ordered item removal. - Remove, + Remove { diagnostics: Vec }, } #[derive(Debug, Default, Clone)] @@ -53,22 +59,138 @@ impl Display for AuxData { } } +/// Diagnostic returned by the procedural macro. +#[derive(Debug)] +pub struct Diagnostic { + pub message: String, + pub severity: Severity, +} + +/// The severity of a diagnostic. +#[derive(Debug)] +pub enum Severity { + Error = 1, + Warning = 2, +} + +#[derive(Debug)] +pub struct Diagnostics(Vec); + +impl Diagnostic { + pub fn error(message: impl ToString) -> Self { + Self { + message: message.to_string(), + severity: Severity::Error, + } + } + + pub fn warn(message: impl ToString) -> Self { + Self { + message: message.to_string(), + severity: Severity::Warning, + } + } +} + +impl From> for Diagnostics { + fn from(diagnostics: Vec) -> Self { + Self(diagnostics) + } +} +impl Diagnostics { + pub fn new(diagnostics: Vec) -> Self { + Self(diagnostics) + } + + pub fn error(mut self, message: impl ToString) -> Self { + self.0.push(Diagnostic::error(message)); + self + } + + pub fn warn(mut self, message: impl ToString) -> Self { + self.0.push(Diagnostic::warn(message)); + self + } +} + +impl IntoIterator for Diagnostics { + type Item = Diagnostic; + type IntoIter = IntoIter; + + fn into_iter(self) -> IntoIter { + self.0.into_iter() + } +} + impl ProcMacroResult { + pub fn leave() -> Self { + Self::Leave { + diagnostics: Vec::new(), + } + } + + pub fn remove() -> Self { + Self::Remove { + diagnostics: Vec::new(), + } + } + + pub fn replace(token_stream: TokenStream, aux_data: Option) -> Self { + Self::Replace { + aux_data, + token_stream, + diagnostics: Vec::new(), + } + } + + pub fn with_diagnostics(mut self, diagnostics: Diagnostics) -> Self { + match &mut self { + Self::Leave { diagnostics: d } => d.extend(diagnostics), + Self::Remove { diagnostics: d } => d.extend(diagnostics), + Self::Replace { diagnostics: d, .. } => d.extend(diagnostics), + }; + self + } + /// Convert to FFI-safe representation. /// /// # Safety #[doc(hidden)] pub fn into_stable(self) -> StableProcMacroResult { match self { - ProcMacroResult::Leave => StableProcMacroResult::Leave, - ProcMacroResult::Remove => StableProcMacroResult::Remove, + ProcMacroResult::Leave { diagnostics } => { + let diagnostics = diagnostics + .into_iter() + .map(|d| d.into_stable()) + .collect::>(); + StableProcMacroResult::Leave { + diagnostics: StableSlice::new(diagnostics), + } + } + ProcMacroResult::Remove { diagnostics } => { + let diagnostics = diagnostics + .into_iter() + .map(|d| d.into_stable()) + .collect::>(); + StableProcMacroResult::Remove { + diagnostics: StableSlice::new(diagnostics), + } + } ProcMacroResult::Replace { token_stream, aux_data, - } => StableProcMacroResult::Replace { - token_stream: token_stream.into_stable(), - aux_data: AuxData::maybe_into_stable(aux_data), - }, + diagnostics, + } => { + let diagnostics = diagnostics + .into_iter() + .map(|d| d.into_stable()) + .collect::>(); + StableProcMacroResult::Replace { + token_stream: token_stream.into_stable(), + aux_data: AuxData::maybe_into_stable(aux_data), + diagnostics: StableSlice::new(diagnostics), + } + } } } @@ -78,17 +200,40 @@ impl ProcMacroResult { /// /// # Safety #[doc(hidden)] - pub unsafe fn from_stable(result: StableProcMacroResult) -> Self { + pub unsafe fn from_stable(result: &StableProcMacroResult) -> Self { match result { - StableProcMacroResult::Leave => ProcMacroResult::Leave, - StableProcMacroResult::Remove => ProcMacroResult::Remove, + StableProcMacroResult::Leave { diagnostics } => { + let (ptr, n) = diagnostics.raw_parts(); + let diagnostics = slice::from_raw_parts(ptr, n) + .iter() + .map(|d| Diagnostic::from_stable(d)) + .collect::>(); + ProcMacroResult::Leave { diagnostics } + } + StableProcMacroResult::Remove { diagnostics } => { + let (ptr, n) = diagnostics.raw_parts(); + let diagnostics = slice::from_raw_parts(ptr, n) + .iter() + .map(|d| Diagnostic::from_stable(d)) + .collect::>(); + ProcMacroResult::Remove { diagnostics } + } StableProcMacroResult::Replace { token_stream, aux_data, - } => ProcMacroResult::Replace { - token_stream: TokenStream::from_stable(token_stream), - aux_data: AuxData::from_stable(aux_data), - }, + diagnostics, + } => { + let (ptr, n) = diagnostics.raw_parts(); + let diagnostics = slice::from_raw_parts(ptr, n) + .iter() + .map(|d| Diagnostic::from_stable(d)) + .collect::>(); + ProcMacroResult::Replace { + token_stream: TokenStream::from_stable(token_stream), + aux_data: AuxData::from_stable(aux_data), + diagnostics, + } + } } } @@ -101,15 +246,38 @@ impl ProcMacroResult { #[doc(hidden)] pub unsafe fn from_owned_stable(result: StableProcMacroResult) -> Self { match result { - StableProcMacroResult::Leave => ProcMacroResult::Leave, - StableProcMacroResult::Remove => ProcMacroResult::Remove, + StableProcMacroResult::Leave { diagnostics } => { + let diagnostics = diagnostics.into_owned(); + let diagnostics = diagnostics + .into_iter() + .map(|d| Diagnostic::from_owned_stable(d)) + .collect::>(); + ProcMacroResult::Leave { diagnostics } + } + StableProcMacroResult::Remove { diagnostics } => { + let diagnostics = diagnostics.into_owned(); + let diagnostics = diagnostics + .into_iter() + .map(|d| Diagnostic::from_owned_stable(d)) + .collect::>(); + ProcMacroResult::Remove { diagnostics } + } StableProcMacroResult::Replace { token_stream, aux_data, - } => ProcMacroResult::Replace { - token_stream: TokenStream::from_owned_stable(token_stream), - aux_data: AuxData::from_owned_stable(aux_data), - }, + diagnostics, + } => { + let diagnostics = diagnostics.into_owned(); + let diagnostics = diagnostics + .into_iter() + .map(|d| Diagnostic::from_owned_stable(d)) + .collect::>(); + ProcMacroResult::Replace { + token_stream: TokenStream::from_owned_stable(token_stream), + aux_data: AuxData::from_owned_stable(aux_data), + diagnostics, + } + } } } } @@ -130,7 +298,7 @@ impl TokenStream { /// /// # Safety #[doc(hidden)] - pub unsafe fn from_stable(token_stream: StableTokenStream) -> Self { + pub unsafe fn from_stable(token_stream: &StableTokenStream) -> Self { Self::new(token_stream.to_string()) } @@ -173,10 +341,10 @@ impl AuxData { /// /// # Safety #[doc(hidden)] - pub unsafe fn from_stable(aux_data: StableAuxData) -> Option { + pub unsafe fn from_stable(aux_data: &StableAuxData) -> Option { match aux_data { StableAuxData::None => None, - StableAuxData::Some(raw) => Some(Self::new(from_raw_cstr(raw))), + StableAuxData::Some(raw) => Some(Self::new(from_raw_cstr(*raw))), } } @@ -195,6 +363,69 @@ impl AuxData { } } +impl Diagnostic { + // Convert to FFI-safe representation. + /// + /// # Safety + #[doc(hidden)] + pub fn into_stable(self) -> StableDiagnostic { + StableDiagnostic { + message: CString::new(self.message).unwrap().into_raw(), + severity: self.severity.into_stable(), + } + } + + /// Convert to native Rust representation, without taking the ownership of the string. + /// + /// Note that you still need to free the memory by calling `from_owned_stable`. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_stable(diagnostic: &StableDiagnostic) -> Self { + Self { + message: from_raw_cstr(diagnostic.message), + severity: Severity::from_stable(&diagnostic.severity), + } + } + + /// Convert to native Rust representation, with taking the ownership of the string. + /// + /// Useful when you need to free the allocated memory. + /// Only use on the same side of FFI-barrier, where the memory has been allocated. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_owned_stable(diagnostic: StableDiagnostic) -> Self { + Self { + message: from_raw_cstring(diagnostic.message), + severity: Severity::from_stable(&diagnostic.severity), + } + } +} + +impl Severity { + /// Convert to FFI-safe representation. + /// # Safety + /// + #[doc(hidden)] + pub fn into_stable(self) -> StableSeverity { + NonZeroU8::try_from(self as u8).unwrap() + } + + /// Convert to native Rust representation. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_stable(severity: &StableSeverity) -> Self { + if *severity == Self::Error.into_stable() { + Self::Error + } else { + // Note that it defaults to warning for unknown values. + Self::Warning + } + } +} + // Create a string from a raw pointer to a c_char. // Note that this will free the underlying memory. unsafe fn from_raw_cstring(raw: *mut c_char) -> String { diff --git a/scarb/src/compiler/plugin/proc_macro/ffi.rs b/scarb/src/compiler/plugin/proc_macro/ffi.rs index 309631c65..3f80b653c 100644 --- a/scarb/src/compiler/plugin/proc_macro/ffi.rs +++ b/scarb/src/compiler/plugin/proc_macro/ffi.rs @@ -2,7 +2,7 @@ use crate::core::{Config, Package, PackageId}; use anyhow::{Context, Result}; use cairo_lang_defs::patcher::PatchBuilder; use cairo_lang_macro::{ProcMacroResult, TokenStream}; -use cairo_lang_macro_stable::{StableProcMacroResult, StableTokenStream}; +use cairo_lang_macro_stable::{StableProcMacroResult, StableResultWrapper, StableTokenStream}; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; use camino::Utf8PathBuf; @@ -77,23 +77,23 @@ impl ProcMacroInstance { let stable_token_stream = token_stream.into_stable(); // Call FFI interface for code expansion. // Note that `stable_result` has been allocated by the dynamic library. - let stable_result = (self.plugin.vtable.expand)(stable_token_stream.clone()); + let stable_result = (self.plugin.vtable.expand)(stable_token_stream); // Free the memory allocated by the `stable_token_stream`. // This will call `CString::from_raw` under the hood, to take ownership. unsafe { - TokenStream::from_owned_stable(stable_token_stream); + TokenStream::from_owned_stable(stable_result.input); }; // Create Rust representation of the result. // Note, that the memory still needs to be freed on the allocator side! - let result = unsafe { ProcMacroResult::from_stable(stable_result.clone()) }; + let result = unsafe { ProcMacroResult::from_stable(&stable_result.output) }; // Call FFI interface to free the `stable_result` that has been allocated by previous call. - (self.plugin.vtable.free_result)(stable_result); + (self.plugin.vtable.free_result)(stable_result.output); // Return obtained result. result } } -type ExpandCode = extern "C" fn(StableTokenStream) -> StableProcMacroResult; +type ExpandCode = extern "C" fn(StableTokenStream) -> StableResultWrapper; type FreeResult = extern "C" fn(StableProcMacroResult); struct VTableV0 { diff --git a/scarb/src/compiler/plugin/proc_macro/host.rs b/scarb/src/compiler/plugin/proc_macro/host.rs index 34fdc719d..4d3c36e9b 100644 --- a/scarb/src/compiler/plugin/proc_macro/host.rs +++ b/scarb/src/compiler/plugin/proc_macro/host.rs @@ -1,15 +1,17 @@ use crate::compiler::plugin::proc_macro::{FromItemAst, ProcMacroInstance}; use crate::core::{Config, Package, PackageId}; use anyhow::Result; +use cairo_lang_defs::plugin::PluginDiagnostic; use cairo_lang_defs::plugin::{ DynGeneratedFileAuxData, GeneratedFileAuxData, MacroPlugin, MacroPluginMetadata, PluginGeneratedFile, PluginResult, }; -use cairo_lang_macro::{AuxData, ProcMacroResult, TokenStream}; +use cairo_lang_macro::{AuxData, Diagnostic, ProcMacroResult, Severity, TokenStream}; use cairo_lang_semantic::plugin::PluginSuite; use cairo_lang_syntax::attribute::structured::AttributeListStructurize; -use cairo_lang_syntax::node::ast; use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::ids::SyntaxStablePtrId; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; use itertools::Itertools; use smol_str::SmolStr; use std::any::Any; @@ -141,10 +143,12 @@ impl MacroPlugin for ProcMacroHostPlugin { .into_iter() .chain(self.handle_attribute(db, item_ast.clone())) .chain(self.handle_derive(db, item_ast.clone())); + let stable_ptr = item_ast.clone().stable_ptr().untyped(); let mut token_stream = TokenStream::from_item_ast(db, item_ast); let mut aux_data: Option = None; let mut modified = false; + let mut all_diagnostics: Vec = Vec::new(); for input in expansions { let instance = self .macros @@ -155,19 +159,24 @@ impl MacroPlugin for ProcMacroHostPlugin { ProcMacroResult::Replace { token_stream: new_token_stream, aux_data: new_aux_data, + diagnostics, } => { token_stream = new_token_stream; aux_data = new_aux_data; modified = true; + all_diagnostics.extend(diagnostics); } - ProcMacroResult::Remove => { + ProcMacroResult::Remove { diagnostics } => { + all_diagnostics.extend(diagnostics); return PluginResult { + diagnostics: into_cairo_diagnostics(all_diagnostics, stable_ptr), code: None, - diagnostics: Vec::new(), remove_original_item: true, - } + }; + } + ProcMacroResult::Leave { diagnostics } => { + all_diagnostics.extend(diagnostics); } - ProcMacroResult::Leave => {} }; } if modified { @@ -179,11 +188,15 @@ impl MacroPlugin for ProcMacroHostPlugin { aux_data: aux_data .map(|ad| DynGeneratedFileAuxData::new(ProcMacroAuxData(ad.to_string()))), }), - diagnostics: Vec::new(), + diagnostics: into_cairo_diagnostics(all_diagnostics, stable_ptr), remove_original_item: true, } } else { - PluginResult::default() + PluginResult { + code: None, + diagnostics: into_cairo_diagnostics(all_diagnostics, stable_ptr), + remove_original_item: false, + } } } @@ -195,6 +208,23 @@ impl MacroPlugin for ProcMacroHostPlugin { } } +fn into_cairo_diagnostics( + diagnostics: Vec, + stable_ptr: SyntaxStablePtrId, +) -> Vec { + diagnostics + .into_iter() + .map(|diag| PluginDiagnostic { + stable_ptr, + message: diag.message, + severity: match diag.severity { + Severity::Error => cairo_lang_diagnostics::Severity::Error, + Severity::Warning => cairo_lang_diagnostics::Severity::Warning, + }, + }) + .collect_vec() +} + /// A Scarb wrapper around the `ProcMacroHost` compiler plugin. /// /// This struct represent the compiler plugin in terms of Scarb data model. diff --git a/scarb/tests/build_cairo_plugin.rs b/scarb/tests/build_cairo_plugin.rs index ae08c3d6e..4882744cf 100644 --- a/scarb/tests/build_cairo_plugin.rs +++ b/scarb/tests/build_cairo_plugin.rs @@ -59,31 +59,21 @@ fn lib_path(lib_name: &str) -> String { serde_json::to_string(&path).unwrap() } -fn simple_project(t: &impl PathChild) { +fn simple_project_with_code(t: &impl PathChild, code: impl ToString) { let macro_lib_path = lib_path("cairo-lang-macro"); let macro_stable_lib_path = lib_path("cairo-lang-macro-stable"); CairoPluginProjectBuilder::start() .scarb_project(|b| { - b.name("hello") + b.name("some") .version("1.0.0") .manifest_extra(r#"[cairo-plugin]"#) }) - .lib_rs(indoc! {r#" - use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons}; - - macro_commons!(); - - #[attribute_macro] - pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { - let _code = token_stream.to_string(); - ProcMacroResult::Leave - } - "#}) + .lib_rs(code) .src( "Cargo.toml", formatdoc! {r#" [package] - name = "proc-macro-stub" + name = "some" version = "0.1.0" edition = "2021" publish = false @@ -99,6 +89,21 @@ fn simple_project(t: &impl PathChild) { .build(t); } +fn simple_project(t: &impl PathChild) { + let code = indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons}; + + macro_commons!(); + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + ProcMacroResult::Leave { diagnostics: Vec::new() } + } + "#}; + simple_project_with_code(t, code); +} + #[test] fn compile_cairo_plugin() { let t = TempDir::new().unwrap(); @@ -117,7 +122,7 @@ fn compile_cairo_plugin() { String::from_utf8_lossy(&output.stderr), ); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(stdout.contains("Compiling hello v1.0.0")); + assert!(stdout.contains("Compiling some v1.0.0")); let lines = stdout.lines().map(ToString::to_string).collect::>(); let (last, lines) = lines.split_last().unwrap(); assert_matches(r#"[..] Finished release target(s) in [..]"#, last); @@ -143,7 +148,7 @@ fn check_cairo_plugin() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - assert!(stdout.contains("Checking hello v1.0.0")); + assert!(stdout.contains("Checking some v1.0.0")); let lines = stdout.lines().map(ToString::to_string).collect::>(); let (last, lines) = lines.split_last().unwrap(); assert_matches(r#"[..] Finished checking release target(s) in [..]"#, last); @@ -193,7 +198,7 @@ fn can_use_json_output() { let lines = stdout.lines().map(ToString::to_string).collect::>(); let (first, lines) = lines.split_first().unwrap(); assert_matches( - r#"{"status":"checking","message":"hello v1.0.0 ([..]Scarb.toml)"}"#, + r#"{"status":"checking","message":"some v1.0.0 ([..]Scarb.toml)"}"#, first, ); let (last, lines) = lines.split_last().unwrap(); @@ -255,3 +260,101 @@ fn compile_cairo_plugin_with_other_target() { target `cairo-plugin` cannot be mixed with other targets "#}); } + +#[test] +fn can_emit_plugin_warning() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + simple_project_with_code( + &t, + indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons, Diagnostic}; + + macro_commons!(); + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + let diag = Diagnostic::warn("Some warning from macro."); + ProcMacroResult::Leave { diagnostics: vec![diag] } + } + "#}, + ); + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + #[some] + fn f() -> felt252 { 12 } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("build") + // Disable output from Cargo. + .env("CARGO_TERM_QUIET", "true") + .current_dir(&project) + .assert() + .success() + .stdout_matches(indoc! {r#" + [..] Compiling some v1.0.0 ([..]Scarb.toml) + [..] Compiling hello v1.0.0 ([..]Scarb.toml) + warn: Plugin diagnostic: Some warning from macro. + --> [..]lib.cairo:1:1 + #[some] + ^*****^ + + [..]Finished release target(s) in [..] + "#}); +} + +#[test] +fn can_emit_plugin_error() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + simple_project_with_code( + &t, + indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, macro_commons, Diagnostic}; + + macro_commons!(); + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + let diag = Diagnostic::error("Some error from macro."); + ProcMacroResult::Leave { diagnostics: vec![diag] } + } + "#}, + ); + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + #[some] + fn f() -> felt252 { 12 } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("build") + // Disable output from Cargo. + .env("CARGO_TERM_QUIET", "true") + .current_dir(&project) + .assert() + .failure() + .stdout_matches(indoc! {r#" + [..] Compiling some v1.0.0 ([..]Scarb.toml) + [..] Compiling hello v1.0.0 ([..]Scarb.toml) + error: Plugin diagnostic: Some error from macro. + --> [..]lib.cairo:1:1 + #[some] + ^*****^ + + error: could not compile `hello` due to previous error + "#}); +}