Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Flavors

Flavors are presets that bundle hooq settings. Built‑in flavors:

NamefeatureContents
default-Default when nothing is specified; overridable via hooq.toml.
empty-Disables hooking; inert attributes still processed. Not overridable.
hook-Inserts a hook method taking hooq::HooqMeta; designed for user traits. Overridable.
anyhowanyhowInserts .with_context(...). Overridable.
eyreeyreInserts .wrap_err_with(...). Overridable.
loglogInserts inspect_err that calls ::log::error!. Overridable.
tracingtracingInserts inspect_err that calls ::tracing::error!. Overridable.

Flavor features are part of the default feature set, so you usually do not need to enable them explicitly.

Users can define flavors in a hooq.toml at crate root.

User‑Defined Flavors

hooq.toml uses table names as flavor names with fields:

FieldTypeDescription
trait_usesarray of stringsTrait paths to import.
methodstringMethod/expression to insert/replace.
hook_targetsarray of stringsAny of "?", "return", "tail_expr".
tail_expr_identsarray of stringsIdents like "Err".
ignore_tail_expr_identsarray of stringsIdents like "Ok".
result_typesarray of stringsReturn type idents like "Result".
hook_in_macrosbooltrue or false.
bindingsinline tableArbitrary bindings; note string literals must be quoted with \".

All built‑in (except empty) can be overridden by defining the same table name. Sub‑tables other than bindings are sub‑flavors and inherit from their parent.

See Attributes for how to apply flavors.

Example hooq.toml:

[my_flavor]
trait_uses = ["sub::MyTrait"]
method = """.inspect_err(|_| {
    let _ = $xxx;
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Err"]
ignore_tail_expr_idents = ["Ok"]
result_types = ["Result"]
hook_in_macros = true
bindings = { xxx = "\"my_flavor\"" }

[my_flavor.sub_flavor]
bindings = { xxx = "\"my_flavor_sub\"" }

Usage:

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

mod sub {
    pub trait MyTrait {}
}

#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::flavor = my_flavor::sub_flavor]
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
mod sub {
    pub trait MyTrait {}
}
#[allow(unused)]
use sub::MyTrait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = "my_flavor";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "my_flavor_sub";
        })?;
    Ok(())
}

default

Default configuration when using #[hooq].

It is configured as follows. (To keep the documentation consistent, this is excerpted directly from the source code; the same applies below.)

use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};

use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};

pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

mod flavor_path;
mod presets;
mod toml_load;

#[derive(Debug, Clone)]
pub struct Flavor {
    pub trait_uses: Vec<Path>,
    pub method: Method,
    pub hook_targets: HookTargetSwitch,
    pub tail_expr_idents: Vec<String>,
    pub ignore_tail_expr_idents: Vec<String>,
    pub result_types: Vec<String>,
    pub hook_in_macros: bool,
    pub bindings: HashMap<String, Rc<Expr>>,
    pub sub_flavors: HashMap<String, Flavor>,
}

impl Default for Flavor {
    fn default() -> Self {
        Self {
            trait_uses: Vec::new(),
            method: default_method(),
            hook_targets: HookTargetSwitch {
                question: true,
                return_: true,
                tail_expr: true,
            },
            tail_expr_idents: vec!["Err".to_string()],
            ignore_tail_expr_idents: vec!["Ok".to_string()],
            result_types: vec!["Result".to_string()],
            hook_in_macros: true,
            bindings: HashMap::new(),
            sub_flavors: HashMap::new(),
        }
    }
}

fn default_method() -> Method {
    // NOTE:
    // $path や $line は eprintln! に直接埋め込みたいところだが、
    // CI側のテストの関係でこのようになっている

    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    let res: TokenStream = parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            ::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
        })
    };

    Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}

#[derive(Debug)]
pub struct FlavorStore {
    flavors: HashMap<String, Flavor>,
}

// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
    pub inner: HooqToml,
}

pub struct TomlStore {
    inner: Mutex<HashMap<String, CheckedHooqToml>>,
}

pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
    inner: Mutex::new(HashMap::new()),
});

impl TomlStore {
    fn load() -> Result<Option<CheckedHooqToml>, String> {
        if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
            return Ok(Some(checked_hooq_toml));
        }

        let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
        let path = PathBuf::from(dir_path).join("hooq.toml");

        if let Ok(false) | Err(_) = path.try_exists() {
            return Ok(None);
        }

        let content = std::fs::read_to_string(&path)
            .map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;

        let hooq_toml: HooqToml = toml::from_str(&content)
            .map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;

        let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;

        LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());

        Ok(Some(checked_hooq_toml))
    }

    // NOTE:
    // 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
    // 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
    //
    // 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
    // プロジェクトごとに保存領域を分けるようにした
    fn set(&self, checked_hooq_toml: CheckedHooqToml) {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().insert(key, checked_hooq_toml);
    }

    fn get(&self) -> Option<CheckedHooqToml> {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().get(&key).cloned()
    }
}

impl FlavorStore {
    fn new() -> Self {
        let flavors = presets::preset_flavors();

        Self { flavors }
    }

    pub fn with_hooq_toml() -> Result<Self, String> {
        let mut flavors = Self::new();

        if let Some(hooq_toml) = TomlStore::load()? {
            // 変換が成功することはCheckedHooqTomlの生成時に確認済み
            toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
        }

        Ok(flavors)
    }

    fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
        let mut path = path.iter();
        let mut current: &Flavor = self.flavors.get(path.next()?)?;

        for name in path {
            current = current.sub_flavors.get(name)?;
        }

        Some(current.clone())
    }

    pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
        self.get_flavor_inner(path).ok_or_else(|| {
            format!(
                "flavor `{}` is not found. available flavors:
{}",
                path.join("::"),
                self.all_flavor_names()
                    .into_iter()
                    .map(|name| format!("  - {name}"))
                    .collect::<Vec<_>>()
                    .join("\n")
            )
        })
    }

    pub fn all_flavor_names(&self) -> Vec<String> {
        fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
            let current_name = format!("{prefix}::");

            for (sub_name, sub_flavor) in &flavor.sub_flavors {
                let full_name = format!("{current_name}{sub_name}");
                names.push(full_name.clone());
                collect_names(sub_flavor, &full_name, names);
            }
        }

        let mut names = Vec::new();

        for (name, flavor) in &self.flavors {
            names.push(name.clone());
            collect_names(flavor, name, &mut names);
        }

        names
    }
}

impl TryFrom<HooqToml> for FlavorStore {
    type Error = String;

    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let mut flavors = Self::new();

        toml_load::apply::update_flavors(&mut flavors.flavors, value)?;

        Ok(flavors)
    }
}

impl TryFrom<HooqToml> for CheckedHooqToml {
    type Error = String;

    // ロード時に変換可能かをあらかじめ確認する
    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let _ = FlavorStore::try_from(value.clone())?;

        Ok(Self { inner: value })
    }
}

Default method (Please ignore Japanese comments):

use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};

use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};

pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

mod flavor_path;
mod presets;
mod toml_load;

#[derive(Debug, Clone)]
pub struct Flavor {
    pub trait_uses: Vec<Path>,
    pub method: Method,
    pub hook_targets: HookTargetSwitch,
    pub tail_expr_idents: Vec<String>,
    pub ignore_tail_expr_idents: Vec<String>,
    pub result_types: Vec<String>,
    pub hook_in_macros: bool,
    pub bindings: HashMap<String, Rc<Expr>>,
    pub sub_flavors: HashMap<String, Flavor>,
}

impl Default for Flavor {
    fn default() -> Self {
        Self {
            trait_uses: Vec::new(),
            method: default_method(),
            hook_targets: HookTargetSwitch {
                question: true,
                return_: true,
                tail_expr: true,
            },
            tail_expr_idents: vec!["Err".to_string()],
            ignore_tail_expr_idents: vec!["Ok".to_string()],
            result_types: vec!["Result".to_string()],
            hook_in_macros: true,
            bindings: HashMap::new(),
            sub_flavors: HashMap::new(),
        }
    }
}

fn default_method() -> Method {
    // NOTE:
    // $path や $line は eprintln! に直接埋め込みたいところだが、
    // CI側のテストの関係でこのようになっている

    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    let res: TokenStream = parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            ::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
        })
    };

    Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}

#[derive(Debug)]
pub struct FlavorStore {
    flavors: HashMap<String, Flavor>,
}

// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
    pub inner: HooqToml,
}

pub struct TomlStore {
    inner: Mutex<HashMap<String, CheckedHooqToml>>,
}

pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
    inner: Mutex::new(HashMap::new()),
});

impl TomlStore {
    fn load() -> Result<Option<CheckedHooqToml>, String> {
        if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
            return Ok(Some(checked_hooq_toml));
        }

        let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
        let path = PathBuf::from(dir_path).join("hooq.toml");

        if let Ok(false) | Err(_) = path.try_exists() {
            return Ok(None);
        }

        let content = std::fs::read_to_string(&path)
            .map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;

        let hooq_toml: HooqToml = toml::from_str(&content)
            .map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;

        let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;

        LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());

        Ok(Some(checked_hooq_toml))
    }

    // NOTE:
    // 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
    // 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
    //
    // 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
    // プロジェクトごとに保存領域を分けるようにした
    fn set(&self, checked_hooq_toml: CheckedHooqToml) {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().insert(key, checked_hooq_toml);
    }

    fn get(&self) -> Option<CheckedHooqToml> {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().get(&key).cloned()
    }
}

impl FlavorStore {
    fn new() -> Self {
        let flavors = presets::preset_flavors();

        Self { flavors }
    }

    pub fn with_hooq_toml() -> Result<Self, String> {
        let mut flavors = Self::new();

        if let Some(hooq_toml) = TomlStore::load()? {
            // 変換が成功することはCheckedHooqTomlの生成時に確認済み
            toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
        }

        Ok(flavors)
    }

    fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
        let mut path = path.iter();
        let mut current: &Flavor = self.flavors.get(path.next()?)?;

        for name in path {
            current = current.sub_flavors.get(name)?;
        }

        Some(current.clone())
    }

    pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
        self.get_flavor_inner(path).ok_or_else(|| {
            format!(
                "flavor `{}` is not found. available flavors:
{}",
                path.join("::"),
                self.all_flavor_names()
                    .into_iter()
                    .map(|name| format!("  - {name}"))
                    .collect::<Vec<_>>()
                    .join("\n")
            )
        })
    }

    pub fn all_flavor_names(&self) -> Vec<String> {
        fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
            let current_name = format!("{prefix}::");

            for (sub_name, sub_flavor) in &flavor.sub_flavors {
                let full_name = format!("{current_name}{sub_name}");
                names.push(full_name.clone());
                collect_names(sub_flavor, &full_name, names);
            }
        }

        let mut names = Vec::new();

        for (name, flavor) in &self.flavors {
            names.push(name.clone());
            collect_names(flavor, name, &mut names);
        }

        names
    }
}

impl TryFrom<HooqToml> for FlavorStore {
    type Error = String;

    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let mut flavors = Self::new();

        toml_load::apply::update_flavors(&mut flavors.flavors, value)?;

        Ok(flavors)
    }
}

impl TryFrom<HooqToml> for CheckedHooqToml {
    type Error = String;

    // ロード時に変換可能かをあらかじめ確認する
    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let _ = FlavorStore::try_from(value.clone())?;

        Ok(Self { inner: value })
    }
}

Usage:

use hooq::hooq;

#[hooq]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    Err("Hello, world!".into())
}

Result:

[mdbook-source-code/flavor-default/src/main.rs:5:5] "Hello, world!"
   5>    Err("Hell..rld!".into())
    |
Error: "Hello, world!"

You can override via hooq.toml.

empty

Disables hooking; intended for conditional builds like #[cfg_attr(feature = "...", hooq(empty))].

use std::collections::HashMap;

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn empty_flavor() -> Flavor {
    Flavor {
        trait_uses: Vec::new(),
        method: empty_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        hook_targets: HookTargetSwitch {
            question: false,
            return_: false,
            tail_expr: false,
        },
        tail_expr_idents: Vec::new(),
        ignore_tail_expr_idents: Vec::new(),
        result_types: Vec::new(),
        hook_in_macros: false,
        bindings: HashMap::new(),
        sub_flavors: HashMap::new(),
    }
}

fn empty_method() -> TokenStream {
    parse_quote! {
        $expr
    }
}

Not overridable.

hook

(Please ignore Japanese comments)

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn hook_flavor() -> Flavor {
    Flavor {
        // NOTE: Traitの存在を前提とするflavorだがユーザーが決定する必要あり
        // trait_uses: Vec::new(), // Default と同じ
        method: hook_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn hook_method() -> TokenStream {
    parse_quote! {
        .hook(|| {
            $hooq_meta
        })
    }
}

Designed for user traits to implement a hook method. It is useful when you do not want to use hooq.toml.

Usage:

use hooq::hooq;

mod my_error {
    pub trait MyHook {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
    }

    impl<T, E> MyHook for Result<T, E>
    where
        E: std::fmt::Debug,
    {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
            if let Err(e) = &self {
                let meta = meta_fn();

                eprintln!(
                    "[{}:{}:{}] error occurred: {:?}",
                    meta.file, meta.line, meta.column, e
                );
            }

            self
        }
    }
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq(hook, trait_uses(my_error::MyHook))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

The second argument is a closure (meta_fn) for lazy evaluation to avoid constructing HooqMeta everywhere.

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod my_error {
    pub trait MyHook {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
    }
    impl<T, E> MyHook for Result<T, E>
    where
        E: std::fmt::Debug,
    {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
            if let Err(e) = &self {
                let meta = meta_fn();
                {
                    ::std::io::_eprint(
                        format_args!(
                            "[{0}:{1}:{2}] error occurred: {3:?}\n",
                            meta.file,
                            meta.line,
                            meta.column,
                            e,
                        ),
                    );
                };
            }
            self
        }
    }
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
#[allow(unused)]
use my_error::MyHook as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .hook(|| {
            ::hooq::HooqMeta {
                line: 33usize,
                column: 17usize,
                path: "mdbook-source-code/flavor-hook/src/main.rs",
                file: "main.rs",
                source_str: "failable(()) ?",
                count: "1st ?",
                bindings: ::std::collections::HashMap::from([]),
            }
        })?;
    Ok(())
}

anyhow

Requires anyhow feature (included in default).

This flavor is intended to be used with the anyhow crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn anyhow_flavor() -> Flavor {
    Flavor {
        trait_uses: vec![parse_quote! { ::anyhow::Context }],
        method: anyhow_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn anyhow_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .with_context(|| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            format!("[{path}:{line}:{col}]\n{expr}")
        })
    }
}

Imports anyhow::Context for .with_context(...).

Usage:

use hooq::hooq;

#[hooq(anyhow)]
fn func1() -> anyhow::Result<i32> {
    Err(anyhow::anyhow!("Error in func1"))
}

#[hooq(anyhow)]
fn func2() -> anyhow::Result<i32> {
    let res = func1()?;

    println!("{res}");

    Ok(res)
}

#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
    func2()?;

    Ok(())
}

Result:

Error: [mdbook-source-code/flavor-anyhow/src/main.rs:19:12]
  19>    func2()?
    |

Caused by:
    0: [mdbook-source-code/flavor-anyhow/src/main.rs:10:22]
         10>    func1()?
           |
    1: [mdbook-source-code/flavor-anyhow/src/main.rs:5:5]
          5>    Err(anyhow::anyhow!("Error in func1"))
           |
    2: Error in func1

eyre

Requires eyre feature (included in default).

This flavor is intended to be used with the eyre crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn eyre_flavor() -> Flavor {
    Flavor {
        trait_uses: vec![parse_quote! { ::eyre::WrapErr }],
        method: eyre_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn eyre_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .wrap_err_with(|| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            format!("[{path}:{line}:{col}]\n{expr}")
        })
    }
}

Imports eyre::WrapErr for .wrap_err_with(...).

Usage:

use hooq::hooq;

#[hooq(eyre)]
fn func1() -> eyre::Result<i32> {
    Err(eyre::eyre!("Error in func1"))
}

#[hooq(eyre)]
fn func2() -> eyre::Result<i32> {
    let res = func1()?;

    println!("{res}");

    Ok(res)
}

#[hooq(eyre)]
fn main() -> eyre::Result<()> {
    func2()?;

    Ok(())
}

Result:

Error: [mdbook-source-code/flavor-eyre/src/main.rs:19:12]
  19>    func2()?
    |

Caused by:
   0: [mdbook-source-code/flavor-eyre/src/main.rs:10:22]
        10>    func1()?
          |
   1: [mdbook-source-code/flavor-eyre/src/main.rs:5:5]
         5>    Err(eyre::eyre!("Error in func1"))
          |
   2: Error in func1

Location:
    mdbook-source-code/flavor-eyre/src/main.rs:5:9

log

Requires log feature (included in default).

This flavor is intended to be used with the log crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn log_flavor() -> Flavor {
    Flavor {
        method: log_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn log_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            ::log::error!("({path}:{line}:{col}) {e}\n{expr}");
        })
    }
}

Usage:

use hooq::hooq;

#[hooq(log)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::Builder::new()
        .format_timestamp(None)
        .filter_level(log::LevelFilter::Error)
        .init();

    Err("Hello, world!".into())
}

Result:

[ERROR flavor_log] (mdbook-source-code/flavor-log/src/main.rs:10:5) Hello, world!
      10>    Err("Hell..rld!".into())
        |
Error: "Hello, world!"

tracing

Requires tracing feature (included in default).

This flavor is intended to be used with the tracing crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn tracing_flavor() -> Flavor {
    Flavor {
        method: tracing_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn tracing_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::one_line_stringify!($source);

            ::tracing::error!(
                path,
                line,
                col,
                error = %e,
                expr,
            );
        })
    }
}

Place #[hooq(tracing)] above #[tracing::instrument] to ensure order.

Usage:

use hooq::hooq;
use tracing::instrument;

#[hooq(tracing)]
#[instrument]
fn func1() -> Result<i32, String> {
    Err("Error in func1".into())
}

#[hooq(tracing)]
#[instrument]
fn func2() -> Result<i32, String> {
    println!("func2 start");

    let res = func1()?;

    println!("func2 end: {res}");

    Ok(res)
}

#[hooq(tracing)]
#[instrument]
fn func3() -> Result<i32, String> {
    println!("func3 start");

    let res = func2()?;

    println!("func3 end: {res}");

    Ok(res)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let format = tracing_subscriber::fmt::format()
        .with_ansi(false)
        .without_time();
    tracing_subscriber::fmt().event_format(format).init();

    func3()?;

    Ok(())
}

Result:

func3 start
func2 start
ERROR func3:func2:func1: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=7 col=5 error=Error in func1 expr="Err(\"Error in func1\".into())"
ERROR func3:func2: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=15 col=22 error=Error in func1 expr="func1()?"
ERROR func3: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=27 col=22 error=Error in func1 expr="func2()?"

Error: "Error in func1"