Как сообщить об ошибках в процедурном макросе, используя макрос цитаты?
Я пишу процедурный макрос, который работает нормально, но у меня возникают проблемы с сообщениями об ошибках эргономичным способом. С помощью panic!
"работает", но не элегантно и не отображает сообщение об ошибке для пользователя.
Я знаю, что могу сообщить о хороших ошибках при разборе 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, я не вижу хорошего способа сообщить об ошибках.
Как это должно быть сделано?
3 ответа
Помимо паники, в настоящее время есть два способа сообщить об ошибках из proc-макроса: нестабильный Diagnostic
API и " compile_error!
трюк ". В настоящее время последний в основном используется, потому что он работает на стабильной. Давайте посмотрим, как они оба работают.
compile_error!
трюк
Поскольку ржавчина 1,20, то compile_error!
макрос существует в стандартной библиотеке. Он принимает строку и приводит к ошибке во время компиляции.
compile_error!("oopsie woopsie");
Что приводит к ( Детская площадка):
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
метод, который возвращает точно поток токенов, который мы создали вручную quote_spanned!
:
syn::Error::new(ty.span(), "expected bool").to_compile_error()
Diagnostic
API
Поскольку это все еще нестабильно, просто короткий пример. API диагностики является более мощным, чем описанный выше трюк, поскольку вы можете иметь несколько интервалов, предупреждений и заметок.
Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();
После этой строки печатается ошибка, но вы все равно можете делать что-то в вашем proc-макросе. Обычно вы просто возвращаете пустой поток токенов.
В принятом ответе упоминается нестабильный API, который дает вам гораздо больше возможностей и контроля, чем обычный
compile_error
. Пока API не стабилизируется, что, вероятно, произойдет не скоро , вы можете использоватьящик. Он обеспечивает тип, который разработан с учетом API-совместимости с нестабильными
proc_macro::Diagnostic
. Реализован не весь API, а только та часть, которую можно разумно реализовать на стабильной версии. Вы можете использовать его, просто добавив предоставленную аннотацию к вашему макросу:
#[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
API, когда он стабилизируется.
Существующие решения просто полагаются на внешние библиотеки, не объясняя, как это на самом деле работает.
Вам не нужно использоватьquote
илиproc-macro2
. Это удобные библиотеки, которые немного облегчают жизнь.
Чтобы сделатьcompiler_error
block, просто буквально сделайте именно это, вот так :
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
enum MyErr {
Nah,
}
fn my_macro_impl(input: TokenStream) -> Result<TokenStream, MyErr> {
let tokens: Vec<TokenTree> = input.into_iter().collect();
if tokens.len() != 0 {
return Err(MyErr::Nah);
}
Ok([TokenTree::Literal(Literal::string("Hello world"))].into_iter().collect())
}
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
match my_macro_impl(input) {
Ok(v) => v,
Err(_) => [
TokenTree::Ident(Ident::new("compile_error", Span::mixed_site())),
TokenTree::Punct(Punct::new('!', Spacing::Alone)),
TokenTree::Group(Group::new(
Delimiter::Parenthesis,
[TokenTree::Literal(Literal::string("Some error message here!"))].into_iter().collect(),
)),
]
.into_iter()
.collect(),
}
}
... и если вы назовете это так:
use my_macros::my_macro;
#[test]
pub fn main() {
assert_eq!(my_macro!("."), "Hello world");
}
Вы получите это при компиляции:
error: Some error message here!
--> tests/000_simple.rs:5:16
|
5 | assert_eq!(my_macro!("."), "Hello world");
| ^^^^^^^^^^^^^^
|
= note: this error originates in the macro `my_macro` (in Nightly builds, run with -Z macro-backtrace for more info)
TokenStream
этоVec
изTokenTree
; если вы хотите отобразить блок:
'compiler_error' '!' '(' 'ошибка' ')'
Тогда AST, который вам нужно вернуть:
- Ident -> 'compiler_error'
- Puct -> '!'
- Group
-- Delimiter::Parenthesis
-- TokenStream
---- Literal -> "my error"
Вот как это работает.
Все остальное — просто «волшебство», облегчающее использование.