diff --git a/Cargo.lock b/Cargo.lock index be23202b9..1b6bc7379 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,6 +732,7 @@ dependencies = [ "anyhow", "cairo-lang-macro-attributes", "cairo-lang-macro-stable", + "libc", "serde", "serde_json", ] diff --git a/plugins/cairo-lang-macro-stable/src/lib.rs b/plugins/cairo-lang-macro-stable/src/lib.rs index 5be4e843d..a7ed41db9 100644 --- a/plugins/cairo-lang-macro-stable/src/lib.rs +++ b/plugins/cairo-lang-macro-stable/src/lib.rs @@ -15,6 +15,26 @@ pub enum StableAuxData { 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. +#[repr(C)] +#[derive(Debug)] +pub enum StableSeverity { + Error, + Warning, +} + /// Procedural macro result. /// /// This struct implements FFI-safe stable ABI. @@ -22,14 +42,22 @@ pub enum StableAuxData { #[derive(Debug)] pub enum StableProcMacroResult { /// Plugin has not taken any action. - Leave, + Leave { + diagnostics: *mut StableDiagnostic, + diagnostics_n: usize, + }, /// Plugin generated [`StableTokenStream`] replacement. Replace { token_stream: StableTokenStream, aux_data: StableAuxData, + diagnostics: *mut StableDiagnostic, + diagnostics_n: usize, }, /// Plugin ordered item removal. - Remove, + Remove { + diagnostics: *mut StableDiagnostic, + diagnostics_n: usize, + }, } impl StableTokenStream { diff --git a/plugins/cairo-lang-macro/Cargo.toml b/plugins/cairo-lang-macro/Cargo.toml index 7ca51d792..0b466f8e4 100644 --- a/plugins/cairo-lang-macro/Cargo.toml +++ b/plugins/cairo-lang-macro/Cargo.toml @@ -14,6 +14,7 @@ repository.workspace = true [dependencies] anyhow.workspace = true +libc.workspace = true cairo-lang-macro-attributes = { path = "../cairo-lang-macro-attributes" } cairo-lang-macro-stable = { path = "../cairo-lang-macro-stable" } serde.workspace = true diff --git a/plugins/cairo-lang-macro/src/lib.rs b/plugins/cairo-lang-macro/src/lib.rs index 2c3546ffa..f50d40336 100644 --- a/plugins/cairo-lang-macro/src/lib.rs +++ b/plugins/cairo-lang-macro/src/lib.rs @@ -1,21 +1,25 @@ +use libc::{free, malloc}; use serde_json::Value; -use std::ffi::{c_char, CString}; +use std::ffi::{c_char, c_void, CString}; use std::fmt::Display; pub use cairo_lang_macro_attributes::*; -use cairo_lang_macro_stable::{StableAuxData, StableProcMacroResult, StableTokenStream}; +use cairo_lang_macro_stable::{ + StableAuxData, StableDiagnostic, StableProcMacroResult, StableSeverity, StableTokenStream, +}; #[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)] @@ -48,6 +52,36 @@ impl 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, + Warning, +} + +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 ProcMacroResult { /// Convert to FFI-safe representation. /// @@ -55,15 +89,33 @@ impl ProcMacroResult { #[doc(hidden)] pub fn into_stable(self) -> StableProcMacroResult { match self { - ProcMacroResult::Leave => StableProcMacroResult::Leave, - ProcMacroResult::Remove => StableProcMacroResult::Remove, + ProcMacroResult::Leave { diagnostics } => { + let (ptr, n) = unsafe { Diagnostic::allocate(diagnostics) }; + StableProcMacroResult::Leave { + diagnostics: ptr, + diagnostics_n: n, + } + } + ProcMacroResult::Remove { diagnostics } => { + let (ptr, n) = unsafe { Diagnostic::allocate(diagnostics) }; + StableProcMacroResult::Remove { + diagnostics: ptr, + diagnostics_n: n, + } + } ProcMacroResult::Replace { token_stream, aux_data, - } => StableProcMacroResult::Replace { - token_stream: token_stream.into_stable(), - aux_data: AuxData::maybe_into_stable(aux_data), - }, + diagnostics, + } => { + let (ptr, n) = unsafe { Diagnostic::allocate(diagnostics) }; + StableProcMacroResult::Replace { + token_stream: token_stream.into_stable(), + aux_data: AuxData::maybe_into_stable(aux_data), + diagnostics: ptr, + diagnostics_n: n, + } + } } } @@ -73,15 +125,33 @@ impl ProcMacroResult { #[doc(hidden)] pub unsafe fn from_stable(result: StableProcMacroResult) -> Self { match result { - StableProcMacroResult::Leave => ProcMacroResult::Leave, - StableProcMacroResult::Remove => ProcMacroResult::Remove, + StableProcMacroResult::Leave { + diagnostics, + diagnostics_n, + } => { + let diagnostics = Diagnostic::deallocate(diagnostics, diagnostics_n); + ProcMacroResult::Leave { diagnostics } + } + StableProcMacroResult::Remove { + diagnostics, + diagnostics_n, + } => { + let diagnostics = Diagnostic::deallocate(diagnostics, diagnostics_n); + 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).unwrap(), - }, + diagnostics, + diagnostics_n, + } => { + let diagnostics = Diagnostic::deallocate(diagnostics, diagnostics_n); + ProcMacroResult::Replace { + token_stream: TokenStream::from_stable(token_stream), + aux_data: AuxData::from_stable(aux_data).unwrap(), + diagnostics, + } + } } } } @@ -138,6 +208,91 @@ impl AuxData { } } +impl Diagnostic { + /// Convert to FFI-safe representation. + /// + /// # Safety + #[doc(hidden)] + pub fn into_stable(self) -> StableDiagnostic { + let cstr = CString::new(self.message).unwrap(); + StableDiagnostic { + message: cstr.into_raw(), + severity: self.severity.into_stable(), + } + } + + /// Convert to native Rust representation. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_stable(diagnostic: StableDiagnostic) -> Self { + Self { + message: raw_to_string(diagnostic.message), + severity: Severity::from_stable(diagnostic.severity), + } + } + + /// Allocate dynamic array with FFI-safe diagnostics. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn allocate(diagnostics: Vec) -> (*mut StableDiagnostic, usize) { + let stable_diagnostics = diagnostics + .into_iter() + .map(|diagnostic| diagnostic.into_stable()) + .collect::>(); + let n = stable_diagnostics.len(); + let ptr = malloc(std::mem::size_of::() * n) as *mut StableDiagnostic; + if ptr.is_null() { + panic!("memory allocation with malloc failed"); + } + for (i, diag) in stable_diagnostics.into_iter().enumerate() { + let ptr = ptr.add(i); + std::ptr::write(ptr, diag); + } + (ptr, n) + } + + /// Deallocate dynamic array of diagnostics, returning a vector. + /// + /// # Safety + pub unsafe fn deallocate(ptr: *mut StableDiagnostic, n: usize) -> Vec { + let mut diagnostics: Vec = Vec::with_capacity(n); + for i in 0..n { + let ptr = ptr.add(i); + let diag = std::ptr::read(ptr); + let diag = Diagnostic::from_stable(diag); + diagnostics.push(diag); + } + free(ptr as *mut c_void); + diagnostics + } +} + +impl Severity { + /// Convert to FFI-safe representation. + /// + /// # Safety + #[doc(hidden)] + pub fn into_stable(self) -> StableSeverity { + match self { + Severity::Error => StableSeverity::Error, + Severity::Warning => StableSeverity::Warning, + } + } + + /// Convert to native Rust representation. + /// + /// # Safety + #[doc(hidden)] + pub unsafe fn from_stable(severity: StableSeverity) -> Self { + match severity { + StableSeverity::Error => Self::Error, + StableSeverity::Warning => Self::Warning, + } + } +} + unsafe fn raw_to_string(raw: *mut c_char) -> String { if raw.is_null() { String::default() diff --git a/scarb/src/compiler/plugin/proc_macro/host.rs b/scarb/src/compiler/plugin/proc_macro/host.rs index 6e36db66a..e1bf95075 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; @@ -135,10 +137,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 @@ -149,19 +153,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 { @@ -173,11 +182,15 @@ impl MacroPlugin for ProcMacroHostPlugin { aux_data: aux_data .map(|ad| DynGeneratedFileAuxData::new(ProcMacroAuxData(ad.to_value()))), }), - 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, + } } } @@ -189,6 +202,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 115c5576a..25e9361bf 100644 --- a/scarb/tests/build_cairo_plugin.rs +++ b/scarb/tests/build_cairo_plugin.rs @@ -59,29 +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}; - - #[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 @@ -97,6 +89,19 @@ 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}; + + #[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(); @@ -112,7 +117,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); @@ -136,7 +141,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); @@ -182,7 +187,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(); @@ -244,3 +249,56 @@ fn compile_cairo_plugin_with_other_target() { target `cairo-plugin` cannot be mixed with other targets "#}); } + +#[test] +fn can_emit_plugin_diagnostics() { + 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, Diagnostic}; + + #[attribute_macro] + pub fn some_macro(token_stream: TokenStream) -> ProcMacroResult { + let _code = token_stream.to_string(); + let diag1 = Diagnostic::warn("Some warning from macro."); + let diag2 = Diagnostic::error("Some error from macro."); + ProcMacroResult::Leave { diagnostics: vec![diag1, diag2] } + } + "#}, + ); + 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) + warn: Plugin diagnostic: Some warning from macro. + --> [..]lib.cairo:1:1 + #[some] + ^*****^ + + error: Plugin diagnostic: Some error from macro. + --> [..]lib.cairo:1:1 + #[some] + ^*****^ + + error: could not compile `hello` due to previous error + "#}); +}