Как сообщить об ошибках в процедурном макросе, используя макрос цитаты? - PullRequest
0 голосов
/ 27 января 2019

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

Как это сделать?

1 Ответ

0 голосов
/ 28 января 2019

Помимо паники, в настоящее время существует два способа сообщения об ошибках из proc-макроса: нестабильный Diagnostic API и "трюк compile_error!" .В настоящее время последний в основном используется, потому что он работает на стабильной.Давайте посмотрим, как они оба работают.

Трюк compile_error!

Начиная с Rust 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!.Но так как мы установили span для неверного входного токена ty, компилятор показывает фрагмент, подчеркивающий этот токен.

Этот прием также используется syn для вывода хороших ошибок.Фактически, если вы все равно используете syn, вы можете использовать его тип Error и, в частности, метод Error::to_compile_error , который возвращает именно тот поток токенов, который мы вручную создали с помощью quote_spanned!:

syn::Error::new(ty.span(), "expected bool").to_compile_error()

API Diagnostic

Поскольку это все еще нестабильно, приведу только короткий пример.API диагностики является более мощным, чем описанный выше трюк, так как вы можете иметь несколько интервалов, предупреждений и примечаний.

Diagnostic::spanned(ty.span().unwrap(), Level::Error, "expected bool").emit();

После этой строки печатается ошибка, но вы все равно можете делать что-то в вашем proc-макросе,Обычно вы просто возвращаете пустой поток токенов.

...