我正在编写一个程序宏,该宏工作正常,但无法以符合人体工程学的方式报告错误。使用panic!
“有效”,但不美观,不能很好地向用户显示错误消息。
我知道我可以在解析TokenStream
时报告错误,但是在解析AST之后遍历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,就看不到报告错误的好方法。
这应该怎么做?
答案 0 :(得分:9)
除了恐慌之外,目前还有两种方法可以报告来自宏程序的错误: 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
。这包括这些标记的跨度:您可以将任何所需的跨度附加到输出标记。所以主要思想是这样:
发出包含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,它返回的正是我们用quote_spanned!
手动创建的令牌流: / p>
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中进行操作。通常,您只会返回一个空的令牌流。
答案 1 :(得分:1)
接受的答案提到了不稳定的 Diagnostic
API,它比常规的 compile_error
为您提供了更多的权力和控制。在 Diagnostic
API 稳定之前,which probably will not be any time soon,您可以使用 proc_macro_error
板条箱。它提供了一个 Diagnostic
类型,旨在与不稳定的 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。