如何使用引用宏报告程序宏中的错误?
How to report errors in a procedural macro using the quote macro?
我正在编写一个工作正常的程序宏,但我无法以符合人体工程学的方式报告错误。使用 panic!
"works" 但不够优雅,无法很好地向用户显示错误消息。
我知道我可以在解析 TokenStream
时报告良好的错误,但我需要在解析后遍历 AST 时产生错误。
宏调用如下所示:
attr_test! {
#[bool]
FOO
}
并且应该输出:
const FOO: bool = false;
这是宏代码:
extern crate proc_macro;
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::{Attribute, parse_macro_input, Ident, Meta};
struct AttrTest {
attributes: Vec<Attribute>,
name: Ident,
}
impl Parse for AttrTest {
fn parse(input: ParseStream) -> Result<Self> {
Ok(AttrTest {
attributes: input.call(Attribute::parse_outer)?,
name: input.parse()?,
})
}
}
#[proc_macro]
pub fn attr_test(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
let test: AttrTest = parse_macro_input!(tokens);
let name = test.name;
let first_att = test.attributes
.get(0)
.and_then(|att| att.parse_meta().ok());
if let Some(Meta::Word(ty)) = first_att {
if ty.to_string() != "bool" {
panic!("expected bool");
}
let output = quote! {
const #name: #ty = false;
};
output.into()
} else {
panic!("malformed or missing metadata")
}
}
如果在属性中指定了 bool
以外的任何内容,我想产生一个错误。比如这样输入:
attr_test! {
#[something_else]
FOO
}
结果应该是这样的:
error: expected bool
attr_test! {
#[something_else]
^^^^^^^^^^^^^^ expected bool
FOO
}
在解析过程中,有一个 Result
,它有很多有用的信息,包括 span
,因此产生的错误可以突出显示宏调用中有问题的确切部分。但是一旦遍历AST,就找不到好的报错方式了。
应该怎么做?
除了 panicing,目前有两种方法可以报告来自 proc-macro 的错误:the unstable Diagnostic
API 和“compile_error!
技巧”。目前,后者主要被使用,因为它可以稳定运行。让我们看看它们是如何工作的。
compile_error!
技巧
自 Rust 1.20 以来,the compile_error!
macro exists in the standard library。它接受一个字符串并在编译时导致错误。
compile_error!("oopsie woopsie");
这导致 (Playground):
error: oopsie woopsie
--> src/lib.rs:1:1
|
1 | compile_error!("oopsie woopsie");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为两种情况添加了此宏:macro_rules!
宏和 #[cfg]
。在这两种情况下,如果用户不正确地使用宏或具有错误的 cfg
值,库作者可以添加更好的错误。
但是 proc-macro 程序员有一个有趣的想法。正如您可能知道的那样,您可以根据自己的喜好创建程序宏中的 TokenStream
和 return。这包括这些标记的跨度:您可以将任何您喜欢的跨度附加到输出标记。所以主要思想是这样的:
发出包含 compile_error!("your error message");
的令牌流,但将这些令牌的跨度设置为导致错误的输入令牌的跨度。 [=] 中甚至还有一个宏 quote
这使得这更容易:quote_spanned!
。对于您的情况,我们可以这样写:
let output = if ty.to_string() != "bool" {
quote_spanned! {
ty.span() =>
compile_error!("expected bool");
}
} else {
quote! {
const #name: #ty = false;
}
};
对于您的错误输入,编译器现在打印如下:
error: expected bool
--> examples/main.rs:4:7
|
4 | #[something_else]
| ^^^^^^^^^^^^^^
为什么这行得通?那么:compile_error!
的错误显示了包含 compile_error!
调用的代码片段。为此,使用了 compile_error!
调用的跨度。但是由于我们将跨度设置为指向错误的输入标记 ty
,编译器会显示该标记下划线的代码段。
这个技巧也被syn
用来打印漂亮的错误。事实上,如果你无论如何都在使用 syn
,你可以使用它的 Error
类型,特别是 Error::to_compile_error
method ,它 return 正是我们用 [= 手动创建的令牌流26=]:
syn::Error::new(ty.span(), "expected bool").to_compile_error()
Diagnostic
API
因为这仍然不稳定,所以只是一个简短的例子。诊断 API 比上面的技巧更强大,因为您可以有多个跨度、警告和注释。
Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
在那行之后,错误被打印出来,但你仍然可以在你的 proc-macro 中做一些事情。通常,您只需要 return 一个空令牌流。
接受的答案提到了不稳定的 Diagnostic
API,它比常规的 compile_error
给你更多的权力和控制。在 Diagnostic
API 稳定之前,which probably will not be any time soon, you can use the proc_macro_error
crate. It provides a Diagnostic
类型被设计为 API 与不稳定的 proc_macro::Diagnostic
兼容。整个API没有实现,只有stable上可以合理实现的部分。您可以通过简单地将提供的注释添加到您的宏来使用它:
#[proc_macro_error]
#[proc_macro]
fn my_macro(input: TokenStream) -> TokenStream {
// ...
Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
}
proc_macro_error
还提供了一些有用的宏来发出错误:
abort! { input,
"I don't like this part!";
note = "A notice message...";
help = "A help message...";
}
但是,您可能要考虑坚持使用 Diagnostic
类型,因为当它稳定后,它会更容易迁移到官方 Diagnostic
API。
我正在编写一个工作正常的程序宏,但我无法以符合人体工程学的方式报告错误。使用 panic!
"works" 但不够优雅,无法很好地向用户显示错误消息。
我知道我可以在解析 TokenStream
时报告良好的错误,但我需要在解析后遍历 AST 时产生错误。
宏调用如下所示:
attr_test! {
#[bool]
FOO
}
并且应该输出:
const FOO: bool = false;
这是宏代码:
extern crate proc_macro;
use quote::quote;
use syn::parse::{Parse, ParseStream, Result};
use syn::{Attribute, parse_macro_input, Ident, Meta};
struct AttrTest {
attributes: Vec<Attribute>,
name: Ident,
}
impl Parse for AttrTest {
fn parse(input: ParseStream) -> Result<Self> {
Ok(AttrTest {
attributes: input.call(Attribute::parse_outer)?,
name: input.parse()?,
})
}
}
#[proc_macro]
pub fn attr_test(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
let test: AttrTest = parse_macro_input!(tokens);
let name = test.name;
let first_att = test.attributes
.get(0)
.and_then(|att| att.parse_meta().ok());
if let Some(Meta::Word(ty)) = first_att {
if ty.to_string() != "bool" {
panic!("expected bool");
}
let output = quote! {
const #name: #ty = false;
};
output.into()
} else {
panic!("malformed or missing metadata")
}
}
如果在属性中指定了 bool
以外的任何内容,我想产生一个错误。比如这样输入:
attr_test! {
#[something_else]
FOO
}
结果应该是这样的:
error: expected bool
attr_test! {
#[something_else]
^^^^^^^^^^^^^^ expected bool
FOO
}
在解析过程中,有一个 Result
,它有很多有用的信息,包括 span
,因此产生的错误可以突出显示宏调用中有问题的确切部分。但是一旦遍历AST,就找不到好的报错方式了。
应该怎么做?
除了 panicing,目前有两种方法可以报告来自 proc-macro 的错误:the unstable Diagnostic
API 和“compile_error!
技巧”。目前,后者主要被使用,因为它可以稳定运行。让我们看看它们是如何工作的。
compile_error!
技巧
自 Rust 1.20 以来,the compile_error!
macro exists in the standard library。它接受一个字符串并在编译时导致错误。
compile_error!("oopsie woopsie");
这导致 (Playground):
error: oopsie woopsie
--> src/lib.rs:1:1
|
1 | compile_error!("oopsie woopsie");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
为两种情况添加了此宏:macro_rules!
宏和 #[cfg]
。在这两种情况下,如果用户不正确地使用宏或具有错误的 cfg
值,库作者可以添加更好的错误。
但是 proc-macro 程序员有一个有趣的想法。正如您可能知道的那样,您可以根据自己的喜好创建程序宏中的 TokenStream
和 return。这包括这些标记的跨度:您可以将任何您喜欢的跨度附加到输出标记。所以主要思想是这样的:
发出包含 compile_error!("your error message");
的令牌流,但将这些令牌的跨度设置为导致错误的输入令牌的跨度。 [=] 中甚至还有一个宏 quote
这使得这更容易:quote_spanned!
。对于您的情况,我们可以这样写:
let output = if ty.to_string() != "bool" {
quote_spanned! {
ty.span() =>
compile_error!("expected bool");
}
} else {
quote! {
const #name: #ty = false;
}
};
对于您的错误输入,编译器现在打印如下:
error: expected bool
--> examples/main.rs:4:7
|
4 | #[something_else]
| ^^^^^^^^^^^^^^
为什么这行得通?那么:compile_error!
的错误显示了包含 compile_error!
调用的代码片段。为此,使用了 compile_error!
调用的跨度。但是由于我们将跨度设置为指向错误的输入标记 ty
,编译器会显示该标记下划线的代码段。
这个技巧也被syn
用来打印漂亮的错误。事实上,如果你无论如何都在使用 syn
,你可以使用它的 Error
类型,特别是 Error::to_compile_error
method ,它 return 正是我们用 [= 手动创建的令牌流26=]:
syn::Error::new(ty.span(), "expected bool").to_compile_error()
Diagnostic
API
因为这仍然不稳定,所以只是一个简短的例子。诊断 API 比上面的技巧更强大,因为您可以有多个跨度、警告和注释。
Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
在那行之后,错误被打印出来,但你仍然可以在你的 proc-macro 中做一些事情。通常,您只需要 return 一个空令牌流。
接受的答案提到了不稳定的 Diagnostic
API,它比常规的 compile_error
给你更多的权力和控制。在 Diagnostic
API 稳定之前,which probably will not be any time soon, you can use the proc_macro_error
crate. It provides a Diagnostic
类型被设计为 API 与不稳定的 proc_macro::Diagnostic
兼容。整个API没有实现,只有stable上可以合理实现的部分。您可以通过简单地将提供的注释添加到您的宏来使用它:
#[proc_macro_error]
#[proc_macro]
fn my_macro(input: TokenStream) -> TokenStream {
// ...
Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
}
proc_macro_error
还提供了一些有用的宏来发出错误:
abort! { input,
"I don't like this part!";
note = "A notice message...";
help = "A help message...";
}
但是,您可能要考虑坚持使用 Diagnostic
类型,因为当它稳定后,它会更容易迁移到官方 Diagnostic
API。