Rust 属性風マクロを軽くハンズオン


こちらの記事は [Rustマクロ冬期講習アドベントカレンダー](https://qiita.com/advent-calendar/2024/rust-macro) 20日目の記事です！

> アドカレまとめ記事はこちら！: [Rustマクロ作成チートシート！](https://qiita.com/namn1125/items/d601d2cbbf2cc7b5d3dc)

[前回](https://qiita.com/namn1125/items/648e915cad62f21656c0)は関数風マクロについて軽いハンズオンを行いました。今回は属性風マクロに取り組みます！

:::note
**属性風マクロの定義・構造**

`Cargo.toml` に、 `[lib] proc-macro = true` という項目を追加した上で、 `src/lib.rs` に、

```rust:src/lib.rs
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn マクロ名(attr: TokenStream, item: TokenStream) -> TokenStream {
    // ...
}
```

というように `#[proc_macro_attribute]` を付けた関数を定義すると、 `#[マクロ名(アトリビュート)]` みたいに呼び出せる属性風マクロが定義されます。

三種の神器クレート [syn](https://docs.rs/syn/latest/syn/) [quote](https://docs.rs/quote/latest/quote/) [proc-macro2](https://docs.rs/proc-macro2/latest/proc_macro2/) を使って入力の [`attr: proc_macro::TokenStream`](https://doc.rust-lang.org/beta/proc_macro/struct.TokenStream.html), `item: proc_macro::TokenStream` を加工していくことで出力を作ります。

`attr` が `#[マクロ名(アトリビュート)]` のうちの `アトリビュート` にあたる部分で、 `item` がこのマクロを付与した対象(アイテム)になります。

なお、 **属性風マクロが何も出力しないと、入力されたアイテムはコードから除かれることになります**。その分柔軟な書き方が可能というわけです。
:::

# 属性風マクロ「 `dbg_stmts` 」

本マクロを付与した関数のブロック内にある各文の実行前に、文の内容をデバッグ出力するマクロを作ってみようと思います[^dbgstmtsname]！

[^dbgstmtsname]: `println!` マクロを利用しているので、 `print_stmts` のほうが妥当だったかもしれないですね...もともとは `dbg!` マクロを使おうかなと思っていた名残でこのマクロ名になりました。

```rust:Rust
#[dbg_stmts]
fn hoge() {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
}
```

こちらが以下と同等になるように展開されることを目指します！

```rust:Rust
fn hoge() {
    println!("[dbg_stmt] {}", "let hoge = 10;");
    let hoge = 10;
    println!("[dbg_stmt] {}", "let fuga = hoge + 20;");
    let fuga = hoge + 20;
    println!("[dbg_stmt] {}", "let bar = vec![hoge, fuga];");
    let bar = vec![hoge, fuga];
    println!("[dbg_stmt] {}", r#"println!("{:?}", bar);"#);
    println!("{:?}", bar);
}
```

なお、属性風マクロにオプションを渡せるようにします！

例: `#[dbg_stmts(sep = "===")]`

| オプション | 機能 |
|:-|:-|
| `sep` | 出力行間にセパレータを出力する設定。デフォルトでは出力なし |
| `fmt` | 各行の `println!` に渡すフォーマットの指定 |

今回想定しているマクロの最終的な使用方法全体像です。

```rust:Rust
#[dbg_stmts(
    sep = "セパレータ",
    fmt = "カスタムフォーマット: {}",
)]
fn hoge() {
    文1;
    文2;
    ...
    文n;
    ...
}

// ↓ 展開

fn hoge() {
    println!("カスタムフォーマット: {}", "文1");
    println!("セパレータ");
    文1;
    println!("セパレータ");
    println!("カスタムフォーマット: {}", "文2");
    println!("セパレータ");
    文2;
    println!("セパレータ");

    ...

    println!("カスタムフォーマット: {}", "文n");
    println!("セパレータ"); // 文n 開始前のセパレータ
    文n; // ここでの標準出力はセパレータで囲まれる
    println!("セパレータ"); // 文n 実行後のセパレータ

    ...
}
```

# 手順 1. 骨組みを作る

[RTA](https://qiita.com/namn1125/items/b7d9374e3d6d3f64ab65)と同様に準備します！

```bash
cargo new --lib dbg_stmts
```

```diff_toml:Cargo.toml
[package]
name = "dbg_stmts"
version = "0.1.0"
edition = "2021"

+[lib]
+proc-macro = true

[dependencies]
```

[前回](https://qiita.com/namn1125/items/648e915cad62f21656c0)同様扱いにくい `proc-macro` と縁を切るために、 `src/lib.rs` はシンプルに保ち実装は `src/impls.rs` で行うディレクトリ構造にします。

```plain:ディレクトリ構造
.
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── impls.rs
│   ├── lib.rs
│   └── main.rs # マクロの動作確認用
└── target
```

ここまでは前回の関数風マクロとほとんど同じですが、マクロの宣言方法のみ異なります。

```rust:src/lib.rs
mod impls;

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    todo!()
}
```

関数風マクロは引数一つでしたが、属性風マクロは「属性部分(`#[macro(...)]`)」と「アイテム部分(`fn hoge() {...}`の部分)」の2つの引数を持ちます！ただし今回アイテム部分は関数のみ受け取ることにします。

続き。三種の神器を `cargo add` しておきます。

```bash
cargo add proc-macro2 quote
cargo add syn --features full --features extra-traits
```

# 手順 2. 入力をパースする

属性部分は追加機能なので後ほど取り組むとして、目的の機能である行間出力を先に実装します！

```rust:Rust
#[dbg_stmts]
fn hoge() {
    文;
    文;
    ...
}
```

アイテム部分の `input` は `syn::ItemFn` として受け取れればそれでよいので、[前回](https://qiita.com/namn1125/items/648e915cad62f21656c0)と異なり入力用の構造体は特に設けないことにします。(後で属性部分をパースするために結局用意しますが...)

```rust:Rust
let func = parse_macro_input!(input as ItemFn);
```

一旦関数をそのまま出力するように書いておきます。

```rust:src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn dbg_stmts(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let func = parse_macro_input!(item as ItemFn);

    // 行間出力を加える処理をここで行う

    func.into_token_stream().into()
}
```

# 手順 3. 出力を考える

[syn::ItemFn](https://docs.rs/syn/latest/syn/struct.ItemFn.html) を受け取り、 `syn::ItemFn` を出力する、でも良いのですが、 可変参照を受け取り中身を書き換えるほうが( `Span` の再設定が要らないなど)色々と楽なので、今回は後者で行こうと思います！

改変対象である [syn::ItemFn](https://docs.rs/syn/latest/syn/struct.ItemFn.html) は次のような定義になっています。

```rust:Rust
pub struct ItemFn {
    pub attrs: Vec<Attribute>,
    pub vis: Visibility,
    pub sig: Signature,
    pub block: Box<Block>,
}
```

| フィールド名 | 型 | 説明 |
|:-|:-|:-|
| `attrs` | `Vec<`[`Attribute`](https://docs.rs/syn/latest/syn/struct.Attribute.html)`>` | 本マクロ以外に関数に付与されている属性情報 |
| `vis` | [`Visibility`](https://docs.rs/syn/latest/syn/enum.Visibility.html) | `pub` や `pub(crate)` などの可視性 |
| `sig` | [`Signature`](https://docs.rs/syn/latest/syn/struct.Signature.html) | `async fn func(arg: T) -> T` みたいな関数のシグネチャ部分 |
| `block` | `Box<`[`Block`](https://docs.rs/syn/latest/syn/struct.Block.html)`>` | `{}` の中身。関数の処理内容。 **今回の改変対象** |

そして改変対象の [`syn::Block`](https://docs.rs/syn/latest/syn/struct.Block.html) の定義は以下です。

```rust:Rust
pub struct Block {
    pub brace_token: Brace,
    pub stmts: Vec<Stmt>,
}
```

| フィールド名 | 型 | 説明 |
|:-|:-|:-|
| `brace_token` | [`Brace`](https://docs.rs/syn/latest/syn/token/struct.Brace.html) | `{` と `}` のカッコの部分。主にもとの [`Span`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html) を保存する目的で設けられています(多分)。 |
| `stmts` | `Vec<`[`Stmt`](https://docs.rs/syn/latest/syn/enum.Stmt.html)`>` | ブロックを構成する文のリストです。 **今回の改変対象** |

`&mut func.block` で `&mut Block` を得て、 `stmts` に新しい `Vec<Stmt>` を入れ直す形で改変します。書き下す処理のイメージです。

```rust:src/impls.rs
use syn::{parse_quote, Block};

// insert_println_between_stmts(&mut func.block) として呼び出す

pub fn insert_println_between_stmts(block: &mut Block) {
    block.stmts = /* Vec<Stmt>書き換え */;
}
```

では早速いよいよ(？) `/* Vec<Stmt>書き換え */` の部分を実装していきます。

```rust:Rust
block.stmts = block
    .stmts
    .iter()
    // .map系処理で Stmt ごとに新たなパーツを生成
    .collect();
```

map系処理の部分で `.map(|stmt| stmtからTokenStream生成)`　というように書いても良いのですが、それでは最終的な結果は `Vec<Stmt>` ではなく [`TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) になってしまいます。

そこでクロージャ部分は `|stmt| vec![追加のデバッグ出力文, stmt]` となるようにし、二重配列になるので [`flat_map`](https://doc.rust-lang.org/nightly/std/iter/trait.Iterator.html#method.flat_map) を使うことで `Vec<Stmt>` でコレクトするようにします。

```rust:Rust
block.stmts = block
    .stmts
    .iter()
    .flat_map(|stmt| {
        vec![
            /* 追加のデバッグ出力文 */,
            stmt.clone(),
        ]
    })
    .collect();
```

あとは `/* 追加のデバッグ出力文 */` に出力したいデバッグ文を書くだけです！

ここは宣言マクロに似た感じでそのままテンプレートを書きたいので、 [`syn::parse_quote!`](https://docs.rs/syn/latest/syn/macro.parse_quote.html) マクロを利用します。このマクロの結果は [`TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) ではなく型推論の上で `syn::Stmt` になります。

```rust:Rust
parse_quote! { println!("[dbg_stmts] {}", stringify!(#stmt)); },
```

`parse_quote!` の中身の `println!` では、デバッグ出力したい文部分を [`stringify!`](https://doc.rust-lang.org/std/macro.stringify.html) マクロで囲んでいます。トークン木を渡して出力させるとダブルクォート `"` で囲んでそのまま文字列にしてくれるとても便利なマクロです！

ここまでで全体像は次のようになります。

```rust:src/impls.rs
use syn::{parse_quote, Block};

pub fn insert_println_between_stmts(block: &mut Block) {
    block.stmts = block
        .stmts
        .iter()
        .flat_map(|stmt| {
            vec![
                parse_quote! { println!("[dbg_stmts] {}", stringify!(#stmt)); },
                stmt.clone(),
            ]
        })
        .collect();
}
```

```rust:src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::insert_println_between_stmts;

#[proc_macro_attribute]
pub fn dbg_stmts(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);

    insert_println_between_stmts(&mut func.block);

    func.into_token_stream().into()
}
```

呼び出し側は次のように呼び出せます。

```rust:src/main.rs
use dbg_stmts::dbg_stmts;

#[dbg_stmts]
#[allow(unused)]
fn hoge() -> u32 {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
    100
}

fn main() {
    let _ = hoge();
}
```

`cargo expand` した結果は次のようになり、

```rust:cargo expand
$ cargo expand --bin dbg_stmts
    Checking dbg_stmts v0.1.0 (/path/to/dbg_stmts)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use dbg_stmts::dbg_stmts;
#[allow(unused)]
fn hoge() -> u32 {
    {
        ::std::io::_print(format_args!("[dbg_stmts] {0}\n", "let hoge = 10;"));
    };
    let hoge = 10;
    {
        ::std::io::_print(format_args!("[dbg_stmts] {0}\n", "let fuga = hoge + 20;"));
    };
    let fuga = hoge + 20;
    {
        ::std::io::_print(
            format_args!("[dbg_stmts] {0}\n", "let bar = vec! [hoge, fuga];"),
        );
    };
    let bar = <[_]>::into_vec(#[rustc_box] ::alloc::boxed::Box::new([hoge, fuga]));
    {
        ::std::io::_print(
            format_args!("[dbg_stmts] {0}\n", "println! (\"{:?}\", bar);"),
        );
    };
    {
        ::std::io::_print(format_args!("{0:?}\n", bar));
    };
    {
        ::std::io::_print(format_args!("[dbg_stmts] {0}\n", "100"));
    };
    100
}
fn main() {
    let _ = hoge();
}
```

出力結果は次のとおりになりました。とりあえず基本機能は完成したようです！

```plain:出力結果
$ cargo run -q
[dbg_stmts] let hoge = 10;
[dbg_stmts] let fuga = hoge + 20;
[dbg_stmts] let bar = vec! [hoge, fuga];
[dbg_stmts] println! ("{:?}", bar);
[10, 30]
[dbg_stmts] 100
```

# 手順 4. 属性(アトリビュート)にあるオプションの実装

基本機能ができたので、ここからは `sep` や `fmt` といった属性オプション [^attroption] を受け取りマクロの出力結果を調整する機能を作りこんでいきます！

[^attroption]: この部分を属性やアトリビュートと呼んでしまうと `#[dbg_stmts` と `]` の部分はなんなんだとなってしまうためこのような呼称にしました。フラグメント指定子では `meta` とされるのでメタ部分と呼んでもよさそうですが、「属性オプション」の方が伝わりやすいと思います。

```rust:Rust
#[dbg_stmts(
    // ↓ 受け取れるようにする
    sep = "セパレータ",
    fmt = "カスタムフォーマット: {}",
)]
fn hoge() {
    文1;
    文2;
    ...
}
```

第一引数の `attr: TokenStream` で属性オプション部分を受け取れます。 `dbg!` マクロを利用してどんなトークン木を受け取れているか確認してみます。

```diff_rust:Rust
#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, /* <- 属性部分の入力！ */ item: TokenStream) -> TokenStream {
+    dbg!(&attr);

    let mut func = parse_macro_input!(item as ItemFn);

    insert_println_between_stmts(&mut func.block);

    func.into_token_stream().into()
}
```

```plain
[src/lib.rs:11:5] &attr = TokenStream [
    Ident {
        ident: "sep",
        span: #0 bytes(86..89),
    },
    Punct {
        ch: '=',
        spacing: Alone,
        span: #0 bytes(90..91),
    },
    Literal {
        kind: Str,
        symbol: "セパレータ",
        suffix: None,
        span: #0 bytes(92..109),
    },
    Punct {
        ch: ',',
        spacing: Alone,
        span: #0 bytes(109..110),
    },
    Ident {
        ident: "fmt",
        span: #0 bytes(115..118),
    },
    Punct {
        ch: '=',
        spacing: Alone,
        span: #0 bytes(119..120),
    },
    Literal {
        kind: Str,
        symbol: "カスタムフォーマット: {}",
        suffix: None,
        span: #0 bytes(121..157),
    },
    Punct {
        ch: ',',
        spacing: Alone,
        span: #0 bytes(157..158),
    },
]
```

`#[dbg_stmts( ... )]` の `...` にあたる部分をそのままトークン木として受け取れています。これをこちらで用意した以下の構造体にパースできるようにします。

```rust:Rust
#[derive(Debug)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    pub fmt: LitStr,
}
```

セパレータ `sep` は `Option` 型にし、 `Some` の時だけ出力することにします。出力フォーマット `fmt` も指定に関してはオプションですが、出力には必ず必要なものなので `unwrap_or` を利用して得ることにします。

`parse_macro_input!(attr as DbgStmtsOption)` として `DbgStmtsOption` を得たいので、 [`syn::Parse`](https://docs.rs/syn/latest/syn/parse/trait.Parse.html) を `DbgStmtsOption` にimplします。

```rust:src/impls.rs
impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        todo!()
    }
}
```

次の方針で実装していきます。

- 1: `,` で区切られて `sep = "..."` と `fmt = "..."` が入ってきます。そのため [`parse_terminated`](https://docs.rs/syn/latest/syn/parse/struct.ParseBuffer.html#method.parse_terminated) を利用して [`MetaNameValue`](https://docs.rs/syn/latest/syn/struct.MetaNameValue.html) でイイカンジに受けます
- 2: 得た [`MetaNameValue`](https://docs.rs/syn/latest/syn/struct.MetaNameValue.html) ごとにその `value` の `"..."` 部分を得て、必要なものを保存します
  - 2.1: 各 `value` について `parse_quote!` マクロを使うことで [`LitStr`](https://docs.rs/syn/latest/syn/struct.LitStr.html) として `"..."` を得ます
    - この方法だとパースに失敗した場合(例えば `sep = "..."` が `sep = 0` みたいに間違っている場合)にマクロがパニックしてしまうので、[発展](#%E7%99%BA%E5%B1%95-%E6%AD%A3%E3%81%97%E3%81%84%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%8F%E3%83%B3%E3%83%89%E3%83%AA%E3%83%B3%E3%82%B0)で丁寧なエラーハンドリングに直します
  - 2.2: `sep` か `fmt` の時にそれぞれの変数に `LitStr` 型の値を保存します
- 3: `DbgStmtsOption` 構造体にキャプチャしたオプションをまとめて、返します
  - 3.1: `unwrap_or` を利用し `fmt` 未指定の時は規定値として `LitStr::new("[dbg_stmt] {}", Span::mixed_site())` を作り返します

```rust:src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

// ...省略...

impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        // 1
        // path1 = "value1", path2 = "value2", ... を vec![path = "value"] のようなイテレータとして扱いたい
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        // 2
        let mut sep = None;
        let mut fmt = None;
        // path = "value" を一つずつ
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            // 2.1
            let lit: LitStr = parse_quote!( #value ); // "value" のパース

            // 2.2
            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        // 3
        Ok(Self {
            sep,
            // 3.1
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}
```

`insert_println_between_stmts` も大幅に書き換えていきます。

- 1: `DbgStmtsOption` を受け取れるようにします
  - かっこつけて分解構文を使ってこの時点で `sep` と `fmt` に分けています
- 2: 関数の最後が式で終わっている場合最後のセパレータを出力すると式の評価値を返せなくなります。これを防止するため、 `.peekable()` メソッドで最後かどうかを確認して最後の文の後にはセパレータを出力しないようにします
  - 2.1: 伴い、`flat_map` を使用していた箇所で `while let` を使うようにしています
  - 2.2, 2.3: `sep` が `Some` の時だけセパレータを出力するようにしています
  - 2.3: 最後でない( `stmts.peek().is_some()` が真の)時だけセパレータを出力するようにしています
- 3: `println!("[dbg_stmts] {}", stringify!(#stmt));` とハードコードしていた箇所について、`println!(#fmt, stringify!(#stmt));` として `fmt` を設定するようにしています
  - `fmt` は `LitStr` なのでダブルクォートで囲む必要はありません

```rust:src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

pub fn insert_println_between_stmts(
    block: &mut Block,
    // 1
    DbgStmtsOption { sep, fmt }: DbgStmtsOption,
) {
    // 2
    let mut stmts = block.stmts.iter().peekable();

    let mut res = Vec::new();

    // 2.1
    while let Some(stmt) = stmts.next() {
        let mut v = Vec::with_capacity(4);

        // 3
        v.push(parse_quote! { println!(#fmt, stringify!(#stmt)); });

        // 2.2 sepオプション指定時だけ設定
        if let Some(sep) = &sep {
            v.push(parse_quote! { println!(#sep); });
        }

        v.push(stmt.clone());

        // 2.3 sepオプション指定時だけ&末尾以外で設定
        if let (true, Some(sep)) = (stmts.peek().is_some(), &sep) {
            v.push(parse_quote! { println!(#sep); });
        }

        res.extend(v);
    }

    block.stmts = res;
}

// ...省略...
```

最後に呼び出し側の `src/lib.rs` も修正します。

```rust:src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::{insert_println_between_stmts, DbgStmtsOption};

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);
    let option = parse_macro_input!(attr as DbgStmtsOption);

    insert_println_between_stmts(&mut func.block, option);

    func.into_token_stream().into()
}
```

これでオプションを受け取れるようになりました！

```rust:src/main.rs
use dbg_stmts::dbg_stmts;

#[dbg_stmts(
    sep = "セパレータ",
    fmt = "カスタムフォーマット: {}",
)]
#[allow(unused)]
fn hoge() -> u32 {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
    100
}

fn main() {
    let _ = hoge();
}
```

`cargo expand` の結果は長いので今回は実行時標準出力のみ確認しておきます。

```bash
$ cargo run -q
カスタムフォーマット: let hoge = 10;
セパレータ
セパレータ
カスタムフォーマット: let fuga = hoge + 20;
セパレータ
セパレータ
カスタムフォーマット: let bar = vec! [hoge, fuga];
セパレータ
セパレータ
カスタムフォーマット: println! ("{:?}", bar);
セパレータ
[10, 30]
セパレータ
カスタムフォーマット: 100
セパレータ
```

`[10, 30]` の出力や最後のセパレータが1つない等より想定通りであることを確認できました！

# 発展 正しいエラーハンドリング

`Parse` を `DbgStmtsOption` に実装する際、 `"..."` 部分のパースについて記述が煩雑になる関係で ~~説明が面倒だったので~~ 一旦 [`parse_quote`](https://docs.rs/syn/latest/syn/macro.parse_quote.html) を利用して `LitStr` に変換していました。

```rust:src/impls.rs
impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        // ...省略...

        // path1 = "value1", path2 = "value2", ...
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            let lit: LitStr = parse_quote!( #value ); // "value" のパース

            // ...省略...
        }

        // ...省略...
    }
}
```

しかし `parse_quote` は[変換が失敗しないこと](https://docs.rs/syn/latest/syn/macro.parse_quote.html#panics)を前提にしたマクロで、外からの入力をそのまま渡す目的のものではありません。

`parse_quote` でのパニックに限らず、マクロがパニックすると、以下の画像のようにマクロに渡したアイテム全体に赤線が引かれてしまいます。

![コンパイルエラー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/142683b9-b9a3-b6c7-4267-03170c60d9da.png)

詳細は [Rust手続きマクロ エラーハンドリング手法](https://qiita.com/namn1125/items/f71f204264aa7cd0a6ab) にて解説していますが、 `Parse` トレイトの [`parse`](https://docs.rs/syn/latest/syn/parse/trait.Parse.html#tymethod.parse) メソッドに関してはパニックさせず [`syn::Result`](https://docs.rs/syn/latest/syn/type.Result.html) を返すようにすれば適切なエラーハンドリングができるようになります。

```diff_rust:src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        let mut sep = None;
        let mut fmt = None;
        for MetaNameValue { path, value, .. } in meta_name_value_list {
-            let lit: LitStr = parse_quote!( #value );
+            let Expr::Lit(ExprLit {
+                lit: Lit::Str(lit), ..
+            }) = value
+            else {
+                return Err(Error::new_spanned(value, "expected string literal"));
+            };

            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        Ok(Self {
            sep,
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}
```

詳細を解説します。 `MetaNameValue` で分解した `value` は今列挙体 [`syn::Expr`](https://docs.rs/syn/latest/syn/enum.Expr.html) になっています。ここから内側の値をパターンマッチで取っていき、 [`LitStr`](https://docs.rs/syn/latest/syn/struct.LitStr.html) までマッチさせて変数 `lit` に代入させています。

列挙体なので論駁可能であり、そのため `let-else` 文を利用しています。失敗した時には [`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) を返します。

```rust:Rust
let Expr::Lit(ExprLit {
   lit: Lit::Str(lit), ..
}) = value
else {
   return Err(Error::new_spanned(value, "expected string literal"));
};
```

[`Error::new_spanned`](https://docs.rs/syn/latest/syn/struct.Error.html#method.new_spanned) の部分が今回のミソで、マクロがエラーとなった原因箇所を赤下線でハイライトさせるために、エラーの該当箇所の情報( [`proc_macro::Span`](https://doc.rust-lang.org/proc_macro/struct.Span.html) )を持つ `value` を第一引数に渡しています。

これで、マクロはパニックせずコンパイルエラー要因となる箇所を正しく提示できるようになります！

![コンパイルエラー2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/076e3164-c7cb-4242-8bb5-188cbf35a6ec.png)

# まとめ・所感

最終的な全体像は次の通りになります！

```rust:src/lib.rs
mod impls;

use proc_macro::TokenStream;
use quote::ToTokens;
use syn::{parse_macro_input, ItemFn};

use impls::{insert_println_between_stmts, DbgStmtsOption};

#[proc_macro_attribute]
pub fn dbg_stmts(attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut func = parse_macro_input!(item as ItemFn);
    let option = parse_macro_input!(attr as DbgStmtsOption);

    insert_println_between_stmts(&mut func.block, option);

    func.into_token_stream().into()
}
```

```rust:src/impls.rs
use proc_macro2::Span;
use syn::{
    parse::{Parse, ParseStream},
    parse_quote, Block, Error, Expr, ExprLit, Lit, LitStr, MetaNameValue, Result, Token,
};

pub fn insert_println_between_stmts(
    block: &mut Block,
    DbgStmtsOption { sep, fmt }: DbgStmtsOption,
) {
    let mut stmts = block.stmts.iter().peekable();

    let mut res = Vec::new();

    while let Some(stmt) = stmts.next() {
        let mut v = Vec::with_capacity(4);

        v.push(parse_quote! { println!(#fmt, stringify!(#stmt)); });

        if let Some(sep) = &sep {
            v.push(parse_quote! { println!(#sep); });
        }

        v.push(stmt.clone());

        if let (true, Some(sep)) = (stmts.peek().is_some(), &sep) {
            v.push(parse_quote! { println!(#sep); });
        }

        res.extend(v);
    }

    block.stmts = res;
}

#[derive(Debug)]
pub struct DbgStmtsOption {
    pub sep: Option<LitStr>,
    pub fmt: LitStr,
}

impl Parse for DbgStmtsOption {
    fn parse(input: ParseStream) -> Result<Self> {
        let meta_name_value_list = input.parse_terminated(MetaNameValue::parse, Token![,])?;

        let mut sep = None;
        let mut fmt = None;
        for MetaNameValue { path, value, .. } in meta_name_value_list {
            let Expr::Lit(ExprLit {
                lit: Lit::Str(lit), ..
            }) = value
            else {
                return Err(Error::new_spanned(value, "expected string literal"));
            };

            match path {
                p if p.is_ident("sep") => sep = Some(lit),
                p if p.is_ident("fmt") => fmt = Some(lit),
                _ => (),
            }
        }

        Ok(Self {
            sep,
            fmt: fmt.unwrap_or(LitStr::new("[dbg_stmt] {}", Span::mixed_site())),
        })
    }
}
```

```rust:src/main.rs
use dbg_stmts::dbg_stmts;

#[dbg_stmts(sep = "=========", fmt = "### {} ###")]
#[allow(unused)]
fn hoge() -> u32 {
    let hoge = 10;
    let fuga = hoge + 20;
    let bar = vec![hoge, fuga];
    println!("{:?}", bar);
    100
}

fn main() {
    let _ = hoge();
}
```

そして `cargo expand` の結果は次のようになり(長いので折りたたんでおきました)、

<details><summary>cargo expand</summary>

```rust:cargo expand
$ cargo expand --bin dbg_stmts
    Checking dbg_stmts v0.1.0 (/path/to/dbg_stmts)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use dbg_stmts::dbg_stmts;
#[allow(unused)]
fn hoge() -> u32 {
    {
        ::std::io::_print(format_args!("### {0} ###\n", "let hoge = 10;"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    let hoge = 10;
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "let fuga = hoge + 20;"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    let fuga = hoge + 20;
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "let bar = vec! [hoge, fuga];"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    let bar = <[_]>::into_vec(#[rustc_box] ::alloc::boxed::Box::new([hoge, fuga]));
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "println! (\"{:?}\", bar);"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("{0:?}\n", bar));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    {
        ::std::io::_print(format_args!("### {0} ###\n", "100"));
    };
    {
        ::std::io::_print(format_args!("=========\n"));
    };
    100
}
fn main() {
    let _ = hoge();
}
```
</details>

実行結果では次のとおり次に実行される文を確認できるようになりました！

```plain:実行結果
$ cargo run -q
### let hoge = 10; ###
=========
=========
### let fuga = hoge + 20; ###
=========
=========
### let bar = vec! [hoge, fuga]; ###
=========
=========
### println! ("{:?}", bar); ###
=========
[10, 30]
=========
### 100 ###
=========
```

関数マクロと同様に、

- `syn` クレートを利用して入力をパース
- 入力となる関数 `func` のフィールドを書き換えることで結果を生成する
  - [`syn::parse_quote!`](https://docs.rs/syn/latest/syn/macro.parse_quote.html) マクロを利用して挿入したい文を生成する
- 属性オプション部分も同じようにパースして利用する
- [`syn::Error`](https://docs.rs/syn/latest/syn/struct.Error.html) を使った正しいエラーハンドリングをする

という手順を解説し、文の間にデバッグ出力を行う属性風マクロを作りました！

アイテムにしか付けられないという制約はありますが、今回見せたような、受け取ったアイテムの可読性を維持しつつちょっとした記述を追加するという場合に、属性風マクロは重宝しますね！

次回は属性風マクロと似ているのだけども雰囲気が違うderiveマクロに取り組みます！