Как сообщить об ошибках в процедурном макросе, используя макрос цитаты?

Я пишу процедурный макрос, который работает нормально, но у меня возникают проблемы с сообщениями об ошибках эргономичным способом. С помощью 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_errorblock, просто буквально сделайте именно это, вот так :

      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"

Вот как это работает.

Все остальное — просто «волшебство», облегчающее использование.

Другие вопросы по тегам