diff --git a/scarb/src/compiler/plugin/proc_macro/host/attribute.rs b/scarb/src/compiler/plugin/proc_macro/host/attribute.rs index 522947965..e9ecaa122 100644 --- a/scarb/src/compiler/plugin/proc_macro/host/attribute.rs +++ b/scarb/src/compiler/plugin/proc_macro/host/attribute.rs @@ -1,5 +1,5 @@ use crate::compiler::plugin::proc_macro::host::aux_data::{EmittedAuxData, ProcMacroAuxData}; -use crate::compiler::plugin::proc_macro::host::into_cairo_diagnostics; +use crate::compiler::plugin::proc_macro::host::{generate_code_mappings, into_cairo_diagnostics}; use crate::compiler::plugin::proc_macro::{ Expansion, ExpansionKind, ProcMacroHostPlugin, ProcMacroId, TokenStreamBuilder, }; @@ -407,11 +407,12 @@ impl ProcMacroHostPlugin { } let file_name = format!("proc_{}", input.expansion.name); + let code_mappings = generate_code_mappings(&result.token_stream); let content = result.token_stream.to_string(); PluginResult { code: Some(PluginGeneratedFile { name: file_name.into(), - code_mappings: Vec::new(), + code_mappings, content, diagnostics_note: Some(format!( "this error originates in the attribute macro: `{}`", diff --git a/scarb/src/compiler/plugin/proc_macro/host/derive.rs b/scarb/src/compiler/plugin/proc_macro/host/derive.rs index 6c771d88c..3f1f82d61 100644 --- a/scarb/src/compiler/plugin/proc_macro/host/derive.rs +++ b/scarb/src/compiler/plugin/proc_macro/host/derive.rs @@ -1,13 +1,14 @@ use crate::compiler::plugin::proc_macro::host::aux_data::{EmittedAuxData, ProcMacroAuxData}; -use crate::compiler::plugin::proc_macro::host::{into_cairo_diagnostics, DERIVE_ATTR}; +use crate::compiler::plugin::proc_macro::host::{ + generate_code_mappings, into_cairo_diagnostics, DERIVE_ATTR, +}; use crate::compiler::plugin::proc_macro::{ Expansion, ExpansionKind, ProcMacroHostPlugin, ProcMacroId, TokenStreamBuilder, }; -use cairo_lang_defs::patcher::PatchBuilder; use cairo_lang_defs::plugin::{DynGeneratedFileAuxData, PluginGeneratedFile, PluginResult}; -use cairo_lang_macro::{ - AllocationContext, Diagnostic, TokenStream, TokenStreamMetadata, TokenTree, -}; +use cairo_lang_filesystem::ids::CodeMapping; +use cairo_lang_filesystem::span::TextWidth; +use cairo_lang_macro::{AllocationContext, Diagnostic, TokenStream, TokenStreamMetadata}; use cairo_lang_syntax::attribute::structured::{AttributeArgVariant, AttributeStructurize}; use cairo_lang_syntax::node::ast::{Expr, PathSegment}; use cairo_lang_syntax::node::db::SyntaxGroup; @@ -73,7 +74,10 @@ impl ProcMacroHostPlugin { let any_derives = !derives.is_empty(); let ctx = AllocationContext::default(); - let mut derived_code = PatchBuilder::new(db, &item_ast); + let mut derived_code = String::new(); + let mut code_mappings = Vec::new(); + let mut current_width = TextWidth::default(); + for derive in derives.iter() { let token_stream = token_stream_builder.build(&ctx); let result = self.instance(derive.package_id).generate_code( @@ -99,17 +103,15 @@ impl ProcMacroHostPlugin { continue; } - for token in result.token_stream.tokens { - match token { - TokenTree::Ident(token) => { - derived_code.add_str(token.content.as_ref()); - } - } - } + code_mappings.extend(generate_code_mappings_with_offset( + &result.token_stream, + current_width, + )); + current_width = current_width + TextWidth::from_str(&result.token_stream.to_string()); + derived_code.push_str(&result.token_stream.to_string()); } if any_derives { - let derived_code = derived_code.build().0; return Some(PluginResult { code: if derived_code.is_empty() { None @@ -126,7 +128,7 @@ impl ProcMacroHostPlugin { let note = format!("this error originates in {msg}: `{derive_names}`"); Some(PluginGeneratedFile { name: "proc_macro_derive".into(), - code_mappings: Vec::new(), + code_mappings, content: derived_code, aux_data: if aux_data.is_empty() { None @@ -146,3 +148,15 @@ impl ProcMacroHostPlugin { None } } + +fn generate_code_mappings_with_offset( + token_stream: &TokenStream, + offset: TextWidth, +) -> Vec { + let mut mappings = generate_code_mappings(token_stream); + for mapping in &mut mappings { + mapping.span.start = mapping.span.start.add_width(offset); + mapping.span.end = mapping.span.end.add_width(offset); + } + mappings +} diff --git a/scarb/src/compiler/plugin/proc_macro/host/inline.rs b/scarb/src/compiler/plugin/proc_macro/host/inline.rs index 225a1b72d..c8da34d55 100644 --- a/scarb/src/compiler/plugin/proc_macro/host/inline.rs +++ b/scarb/src/compiler/plugin/proc_macro/host/inline.rs @@ -1,5 +1,5 @@ use crate::compiler::plugin::proc_macro::host::aux_data::{EmittedAuxData, ProcMacroAuxData}; -use crate::compiler::plugin::proc_macro::host::into_cairo_diagnostics; +use crate::compiler::plugin::proc_macro::host::{generate_code_mappings, into_cairo_diagnostics}; use crate::compiler::plugin::proc_macro::{ Expansion, ProcMacroId, ProcMacroInstance, TokenStreamBuilder, }; @@ -77,10 +77,11 @@ impl InlineMacroExprPlugin for ProcMacroInlinePlugin { DynGeneratedFileAuxData::new(emitted) }); let content = token_stream.to_string(); + let code_mappings = generate_code_mappings(&token_stream); InlinePluginResult { code: Some(PluginGeneratedFile { name: "inline_proc_macro".into(), - code_mappings: Vec::new(), + code_mappings, content, aux_data, diagnostics_note: Some(format!( diff --git a/scarb/src/compiler/plugin/proc_macro/host/mod.rs b/scarb/src/compiler/plugin/proc_macro/host/mod.rs index 15d1af4b4..c41a23548 100644 --- a/scarb/src/compiler/plugin/proc_macro/host/mod.rs +++ b/scarb/src/compiler/plugin/proc_macro/host/mod.rs @@ -14,7 +14,11 @@ use anyhow::{ensure, Result}; use cairo_lang_defs::plugin::PluginDiagnostic; use cairo_lang_defs::plugin::{MacroPlugin, MacroPluginMetadata, PluginResult}; use cairo_lang_filesystem::db::Edition; -use cairo_lang_macro::{AllocationContext, Diagnostic, Severity, TokenStreamMetadata}; +use cairo_lang_filesystem::ids::{CodeMapping, CodeOrigin}; +use cairo_lang_filesystem::span::{TextOffset, TextSpan, TextWidth}; +use cairo_lang_macro::{ + AllocationContext, Diagnostic, Severity, TokenStream, TokenStreamMetadata, TokenTree, +}; use cairo_lang_semantic::plugin::PluginSuite; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::ids::SyntaxStablePtrId; @@ -259,3 +263,30 @@ impl ProcMacroHost { &self.macros } } + +fn generate_code_mappings(token_stream: &TokenStream) -> Vec { + token_stream + .tokens + .iter() + .scan(TextOffset::default(), |current_pos, token| { + let TokenTree::Ident(token) = token; + let token_width = TextWidth::from_str(token.content.as_ref()); + + let mapping = CodeMapping { + span: TextSpan { + start: *current_pos, + end: current_pos.add_width(token_width), + }, + origin: CodeOrigin::Span(TextSpan { + start: TextOffset::default() + .add_width(TextWidth::new_for_testing(token.span.start as u32)), + end: TextOffset::default() + .add_width(TextWidth::new_for_testing(token.span.end as u32)), + }), + }; + + *current_pos = current_pos.add_width(token_width); + Some(mapping) + }) + .collect() +} diff --git a/scarb/tests/build_cairo_plugin.rs b/scarb/tests/build_cairo_plugin.rs index e83faa782..9d95f395e 100644 --- a/scarb/tests/build_cairo_plugin.rs +++ b/scarb/tests/build_cairo_plugin.rs @@ -984,7 +984,7 @@ fn can_implement_derive_macro() { 32 }} }} - "#}; + "#}; let token_stream = TokenStream::new(vec![TokenTree::Ident(Token::new( code.clone(), @@ -1573,3 +1573,199 @@ fn can_expand_impl_inner_func_attrr() { "#}); } + +#[test] +fn code_mappings_preserve_attribute_error_locations() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + CairoPluginProjectBuilder::default() + .lib_rs(indoc! {r#" + use cairo_lang_macro::{ProcMacroResult, TokenStream, attribute_macro, TokenTree, Token, TextSpan}; + + #[attribute_macro] + pub fn some(_attr: TokenStream, mut token_stream: TokenStream) -> ProcMacroResult { + let token_stream_length = token_stream.to_string().len(); + token_stream.tokens.push(TokenTree::Ident(Token::new(" ", TextSpan { start: token_stream_length + 1, end: token_stream_length + 5 }))); + ProcMacroResult::new(token_stream) + } + "#}) + .build(&t); + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + #[some] + fn f() -> felt252 { + let x = 1; + x = 2; + x + } + "#}) + .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: Cannot assign to an immutable variable. + --> [..]lib.cairo[proc_some]:3:5 + x = 2; + ^***^ + note: this error originates in the attribute macro: `some` + + error: could not compile `hello` due to previous error + "#}); +} + +#[test] +fn code_mappings_preserve_inline_macro_error_locations() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + CairoPluginProjectBuilder::default() + .lib_rs(indoc! {r##" + use cairo_lang_macro::{inline_macro, ProcMacroResult, TokenStream, TokenTree, Token, TextSpan}; + + #[inline_macro] + pub fn some(_token_stream: TokenStream) -> ProcMacroResult { + let mut tokens = Vec::new(); + tokens.push(TokenTree::Ident(Token::new( + "undefined".to_string(), + TextSpan::new(0, 9), + ))); + + ProcMacroResult::new(TokenStream::new(tokens)) + } + "##}) + .build(&t); + + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + fn main() -> felt252 { + let _x = some!(); + 12 + } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("build") + .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: Identifier not found. + --> [..]lib.cairo:1:1 + fn main() -> felt252 { + ^*******^ + + error: could not compile `hello` due to previous error + "#}); +} + +#[test] +fn code_mappings_preserve_derive_error_locations() { + let temp = TempDir::new().unwrap(); + let t = temp.child("some"); + CairoPluginProjectBuilder::default() + .lib_rs(indoc! {r##" + use cairo_lang_macro::{derive_macro, ProcMacroResult, TokenStream, TokenTree, Token, TextSpan}; + + #[derive_macro] + pub fn custom_derive(token_stream: TokenStream) -> ProcMacroResult { + let name = token_stream + .clone() + .to_string() + .lines() + .find(|l| l.starts_with("struct")) + .unwrap() + .to_string() + .replace("struct", "") + .replace("}", "") + .replace("{", "") + .trim() + .to_string(); + + let code = indoc::formatdoc!{r#" + impl SomeImpl{name} of Hello<{name}> {{ + fn world(self: @{name}) -> u8 {{ + 256 + }} + }} + "#}; + + let token_stream = TokenStream::new(vec![TokenTree::Ident(Token::new( + code.clone(), + TextSpan { + start: 0, + end: code.len(), + }, + ))]); + + ProcMacroResult::new(token_stream) + } + "##}) + .add_dep(r#"indoc = "*""#) + .build(&t); + + let project = temp.child("hello"); + ProjectBuilder::start() + .name("hello") + .version("1.0.0") + .dep("some", &t) + .lib_cairo(indoc! {r#" + trait Hello { + fn world(self: @T) -> u8; + } + + #[derive(CustomDerive, Drop)] + struct SomeType {} + + #[derive(CustomDerive, Drop)] + struct AnotherType {} + + fn main() -> u8 { + let a = SomeType {}; + a.world() + } + "#}) + .build(&project); + + Scarb::quick_snapbox() + .arg("build") + .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: The value does not fit within the range of type core::integer::u8. + --> [..]lib.cairo:1:1 + trait Hello { + ^**************^ + note: this error originates in the derive macro: `custom_derive` + + error: The value does not fit within the range of type core::integer::u8. + --> [..]lib.cairo:1:1 + trait Hello { + ^**************^ + note: this error originates in the derive macro: `custom_derive` + + error: could not compile `hello` due to previous error + "#}); +}