use clippy_utils::res::MaybeQPath;
use clippy_utils::{get_attr, higher, sym};
use itertools::Itertools;
use rustc_ast::LitIntType;
use rustc_ast::ast::{LitFloatType, LitKind};
use rustc_data_structures::fx::FxHashMap;
use rustc_hir::def_id::DefId;
use rustc_hir::{
    self as hir, BindingMode, CaptureBy, Closure, ClosureKind, ConstArg, ConstArgKind, CoroutineKind, ExprKind,
    FnRetTy, HirId, Lit, PatExprKind, PatKind, QPath, StmtKind, StructTailExpr,
};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{FloatTy, IntTy, UintTy};
use rustc_session::declare_lint_pass;
use rustc_span::symbol::{Ident, Symbol};
use std::cell::Cell;
use std::fmt::{Display, Formatter};

declare_lint_pass!(
    /// ### What it does
    /// Generates clippy code that detects the offending pattern
    ///
    /// ### Example
    /// ```rust,ignore
    /// // ./tests/ui/my_lint.rs
    /// fn foo() {
    ///     // detect the following pattern
    ///     #[clippy::author]
    ///     if x == 42 {
    ///         // but ignore everything from here on
    ///         #![clippy::author = "ignore"]
    ///     }
    ///     ()
    /// }
    /// ```
    ///
    /// Running `TESTNAME=ui/my_lint cargo uitest` will produce
    /// a `./tests/ui/new_lint.stdout` file with the generated code:
    ///
    /// ```rust,ignore
    /// // ./tests/ui/new_lint.stdout
    /// if ExprKind::If(ref cond, ref then, None) = item.kind
    ///     && let ExprKind::Binary(BinOp::Eq, ref left, ref right) = cond.kind
    ///     && let ExprKind::Path(ref path) = left.kind
    ///     && let ExprKind::Lit(ref lit) = right.kind
    ///     && let LitKind::Int(42, _) = lit.node
    /// {
    ///     // report your lint here
    /// }
    /// ```
    Author => []
);

/// Writes a line of output with indentation added
macro_rules! out {
    ($($t:tt)*) => {
        println!("    {}", format_args!($($t)*))
    };
}

/// The variables passed in are replaced with `&Binding`s where the `value` field is set
/// to the original value of the variable. The `name` field is set to the name of the variable
/// (using `stringify!`) and is adjusted to avoid duplicate names.
/// Note that the `Binding` may be printed directly to output the `name`.
macro_rules! bind {
    ($self:ident $(, $name:ident)+) => {
        $(let $name = & $self.bind(stringify!($name), $name);)+
    };
}

/// Transforms the given `Option<T>` variables into `OptionPat<Binding<T>>`.
/// This displays as `Some($name)` or `None` when printed. The name of the inner binding
/// is set to the name of the variable passed to the macro.
macro_rules! opt_bind {
    ($self:ident $(, $name:ident)+) => {
        $(let $name = OptionPat::new($name.map(|o| $self.bind(stringify!($name), o)));)+
    };
}

/// Creates a `Binding` that accesses the field of an existing `Binding`
macro_rules! field {
    ($binding:ident.$field:ident) => {
        &Binding {
            name: $binding.name.to_string() + stringify!(.$field),
            value: $binding.value.$field,
        }
    };
}

/// Print a condition of a let chain, `chain!(self, "let Some(x) = y")` will print
/// `if let Some(x) = y` on the first call and `    && let Some(x) = y` thereafter
macro_rules! chain {
    ($self:ident, $($t:tt)*) => {
        if $self.first.take() {
            println!("if {}", format_args!($($t)*));
        } else {
            println!("    && {}", format_args!($($t)*));
        }
    }
}

impl<'tcx> LateLintPass<'tcx> for Author {
    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
        check_item(cx, item.hir_id());
    }

    fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) {
        check_item(cx, item.hir_id());
    }

    fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) {
        check_item(cx, item.hir_id());
    }

    fn check_arm(&mut self, cx: &LateContext<'tcx>, arm: &'tcx hir::Arm<'_>) {
        check_node(cx, arm.hir_id, |v| {
            v.arm(&v.bind("arm", arm));
        });
    }

    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'_>) {
        check_node(cx, expr.hir_id, |v| {
            v.expr(&v.bind("expr", expr));
        });
    }

    fn check_stmt(&mut self, cx: &LateContext<'tcx>, stmt: &'tcx hir::Stmt<'_>) {
        match stmt.kind {
            StmtKind::Expr(e) | StmtKind::Semi(e) if has_attr(cx, e.hir_id) => return,
            _ => {},
        }
        check_node(cx, stmt.hir_id, |v| {
            v.stmt(&v.bind("stmt", stmt));
        });
    }
}

fn check_item(cx: &LateContext<'_>, hir_id: HirId) {
    if let Some(body) = cx.tcx.hir_maybe_body_owned_by(hir_id.expect_owner().def_id) {
        check_node(cx, hir_id, |v| {
            v.expr(&v.bind("expr", body.value));
        });
    }
}

fn check_node(cx: &LateContext<'_>, hir_id: HirId, f: impl Fn(&PrintVisitor<'_, '_>)) {
    if has_attr(cx, hir_id) {
        f(&PrintVisitor::new(cx));
        println!("{{");
        println!("    // report your lint here");
        println!("}}");
    }
}

fn paths_static_name(cx: &LateContext<'_>, id: DefId) -> String {
    cx.get_def_path(id)
        .iter()
        .map(Symbol::as_str)
        .filter(|s| !s.starts_with('<'))
        .join("_")
        .to_uppercase()
}

struct Binding<T> {
    name: String,
    value: T,
}

impl<T> Display for Binding<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.name)
    }
}

struct OptionPat<T> {
    pub opt: Option<T>,
}

impl<T> OptionPat<T> {
    fn new(opt: Option<T>) -> Self {
        Self { opt }
    }

    fn if_some(&self, f: impl Fn(&T)) {
        if let Some(t) = &self.opt {
            f(t);
        }
    }
}

impl<T: Display> Display for OptionPat<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match &self.opt {
            None => f.write_str("None"),
            Some(node) => write!(f, "Some({node})"),
        }
    }
}

struct PrintVisitor<'a, 'tcx> {
    cx: &'a LateContext<'tcx>,
    /// Fields are the current index that needs to be appended to pattern
    /// binding names
    ids: Cell<FxHashMap<&'static str, u32>>,
    /// Currently at the first condition in the if chain
    first: Cell<bool>,
}

impl<'a, 'tcx> PrintVisitor<'a, 'tcx> {
    fn new(cx: &'a LateContext<'tcx>) -> Self {
        Self {
            cx,
            ids: Cell::default(),
            first: Cell::new(true),
        }
    }

    fn next(&self, s: &'static str) -> String {
        let mut ids = self.ids.take();
        let out = match *ids.entry(s).and_modify(|n| *n += 1).or_default() {
            // first usage of the name, use it as is
            0 => s.to_string(),
            // append a number starting with 1
            n => format!("{s}{n}"),
        };
        self.ids.set(ids);
        out
    }

    fn bind<T>(&self, name: &'static str, value: T) -> Binding<T> {
        let name = self.next(name);
        Binding { name, value }
    }

    fn option<T: Copy>(&self, option: &Binding<Option<T>>, name: &'static str, f: impl Fn(&Binding<T>)) {
        match option.value {
            None => chain!(self, "{option}.is_none()"),
            Some(value) => {
                let value = &self.bind(name, value);
                chain!(self, "let Some({value}) = {option}");
                f(value);
            },
        }
    }

    fn slice<T>(&self, slice: &Binding<&[T]>, f: impl Fn(&Binding<&T>)) {
        if slice.value.is_empty() {
            chain!(self, "{slice}.is_empty()");
        } else {
            chain!(self, "{slice}.len() == {}", slice.value.len());
            for (i, value) in slice.value.iter().enumerate() {
                let name = format!("{slice}[{i}]");
                f(&Binding { name, value });
            }
        }
    }

    fn destination(&self, destination: &Binding<hir::Destination>) {
        self.option(field!(destination.label), "label", |label| {
            self.ident(field!(label.ident));
        });
    }

    fn ident(&self, ident: &Binding<Ident>) {
        chain!(self, "{ident}.as_str() == {:?}", ident.value.as_str());
    }

    fn symbol(&self, symbol: &Binding<Symbol>) {
        chain!(self, "{symbol}.as_str() == {:?}", symbol.value.as_str());
    }

    fn qpath(&self, qpath: &Binding<&QPath<'_>>, hir_id_binding: &str, hir_id: HirId) {
        if let QPath::LangItem(lang_item, ..) = *qpath.value {
            chain!(self, "matches!({qpath}, QPath::LangItem(LangItem::{lang_item:?}, _))");
        } else if let Some(def_id) = self.cx.qpath_res(qpath.value, hir_id).opt_def_id()
            && !def_id.is_local()
        {
            bind!(self, def_id);
            chain!(
                self,
                "let Some({def_id}) = cx.qpath_res({qpath}, {hir_id_binding}.hir_id).opt_def_id()"
            );
            if let Some(name) = self.cx.tcx.get_diagnostic_name(def_id.value) {
                chain!(self, "cx.tcx.is_diagnostic_item(sym::{name}, {def_id})");
            } else {
                chain!(
                    self,
                    "paths::{}.matches(cx, {def_id}) // Add the path to `clippy_utils::paths` if needed",
                    paths_static_name(self.cx, def_id.value)
                );
            }
        }
    }

    fn maybe_path<'p>(&self, path: &Binding<impl MaybeQPath<'p>>) {
        if let Some(id) = path.value.res(self.cx).opt_def_id()
            && !id.is_local()
        {
            if let Some(lang) = self.cx.tcx.lang_items().from_def_id(id) {
                chain!(self, "{path}.res(cx).is_lang_item(cx, LangItem::{}", lang.name());
            } else if let Some(name) = self.cx.tcx.get_diagnostic_name(id) {
                chain!(self, "{path}.res(cx).is_diag_item(cx, sym::{name})");
            } else {
                chain!(
                    self,
                    "paths::{}.matches_path(cx, {path}) // Add the path to `clippy_utils::paths` if needed",
                    paths_static_name(self.cx, id)
                );
            }
        }
    }

    fn const_arg(&self, const_arg: &Binding<&ConstArg<'_>>) {
        match const_arg.value.kind {
            ConstArgKind::Path(ref qpath) => {
                bind!(self, qpath);
                chain!(self, "let ConstArgKind::Path(ref {qpath}) = {const_arg}.kind");
            },
            ConstArgKind::Anon(anon_const) => {
                bind!(self, anon_const);
                chain!(self, "let ConstArgKind::Anon({anon_const}) = {const_arg}.kind");
                self.body(field!(anon_const.body));
            },
            ConstArgKind::Infer(..) => chain!(self, "let ConstArgKind::Infer(..) = {const_arg}.kind"),
        }
    }

    fn lit(&self, lit: &Binding<Lit>) {
        let kind = |kind| chain!(self, "let LitKind::{kind} = {lit}.node");
        macro_rules! kind {
            ($($t:tt)*) => (kind(format_args!($($t)*)));
        }

        match lit.value.node {
            LitKind::Bool(val) => kind!("Bool({val:?})"),
            LitKind::Char(c) => kind!("Char({c:?})"),
            LitKind::Err(_) => kind!("Err"),
            LitKind::Byte(b) => kind!("Byte({b})"),
            LitKind::Int(i, suffix) => {
                let int_ty = match suffix {
                    LitIntType::Signed(int_ty) => {
                        let t = match int_ty {
                            IntTy::Isize => "Isize",
                            IntTy::I8 => "I8",
                            IntTy::I16 => "I16",
                            IntTy::I32 => "I32",
                            IntTy::I64 => "I64",
                            IntTy::I128 => "I128",
                        };
                        format!("LitIntType::Signed(IntTy::{t})")
                    },
                    LitIntType::Unsigned(uint_ty) => {
                        let t = match uint_ty {
                            UintTy::Usize => "Usize",
                            UintTy::U8 => "U8",
                            UintTy::U16 => "U16",
                            UintTy::U32 => "U32",
                            UintTy::U64 => "U64",
                            UintTy::U128 => "U128",
                        };
                        format!("LitIntType::Unsigned(UintTy::{t})")
                    },
                    LitIntType::Unsuffixed => String::from("LitIntType::Unsuffixed"),
                };
                kind!("Int({i}, {int_ty})");
            },
            LitKind::Float(_, suffix) => {
                let float_ty = match suffix {
                    LitFloatType::Suffixed(suffix_ty) => {
                        let t = match suffix_ty {
                            FloatTy::F16 => "F16",
                            FloatTy::F32 => "F32",
                            FloatTy::F64 => "F64",
                            FloatTy::F128 => "F128",
                        };
                        format!("LitFloatType::Suffixed(FloatTy::{t})")
                    },
                    LitFloatType::Unsuffixed => String::from("LitFloatType::Unsuffixed"),
                };
                kind!("Float(_, {float_ty})");
            },
            LitKind::ByteStr(ref vec, _) => {
                bind!(self, vec);
                kind!("ByteStr(ref {vec})");
                chain!(self, "let [{:?}] = **{vec}", vec.value);
            },
            LitKind::CStr(ref vec, _) => {
                bind!(self, vec);
                kind!("CStr(ref {vec})");
                chain!(self, "let [{:?}] = **{vec}", vec.value);
            },
            LitKind::Str(s, _) => {
                bind!(self, s);
                kind!("Str({s}, _)");
                self.symbol(s);
            },
        }
    }

    fn arm(&self, arm: &Binding<&hir::Arm<'_>>) {
        self.pat(field!(arm.pat));
        match arm.value.guard {
            None => chain!(self, "{arm}.guard.is_none()"),
            Some(expr) => {
                bind!(self, expr);
                chain!(self, "let Some({expr}) = {arm}.guard");
                self.expr(expr);
            },
        }
        self.expr(field!(arm.body));
    }

    #[expect(clippy::too_many_lines)]
    fn expr(&self, expr: &Binding<&hir::Expr<'_>>) {
        if let Some(higher::While { condition, body, .. }) = higher::While::hir(expr.value) {
            bind!(self, condition, body);
            chain!(
                self,
                "let Some(higher::While {{ condition: {condition}, body: {body} }}) \
                = higher::While::hir({expr})"
            );
            self.expr(condition);
            self.expr(body);
            return;
        }

        if let Some(higher::WhileLet {
            let_pat,
            let_expr,
            if_then,
            ..
        }) = higher::WhileLet::hir(expr.value)
        {
            bind!(self, let_pat, let_expr, if_then);
            chain!(
                self,
                "let Some(higher::WhileLet {{ let_pat: {let_pat}, let_expr: {let_expr}, if_then: {if_then} }}) \
                = higher::WhileLet::hir({expr})"
            );
            self.pat(let_pat);
            self.expr(let_expr);
            self.expr(if_then);
            return;
        }

        if let Some(higher::ForLoop { pat, arg, body, .. }) = higher::ForLoop::hir(expr.value) {
            bind!(self, pat, arg, body);
            chain!(
                self,
                "let Some(higher::ForLoop {{ pat: {pat}, arg: {arg}, body: {body}, .. }}) \
                = higher::ForLoop::hir({expr})"
            );
            self.pat(pat);
            self.expr(arg);
            self.expr(body);
            return;
        }

        let kind = |kind| chain!(self, "let ExprKind::{kind} = {expr}.kind");
        macro_rules! kind {
            ($($t:tt)*) => (kind(format_args!($($t)*)));
        }

        match expr.value.kind {
            ExprKind::Let(let_expr) => {
                bind!(self, let_expr);
                kind!("Let({let_expr})");
                self.pat(field!(let_expr.pat));
                if let Some(ty) = let_expr.value.ty {
                    bind!(self, ty);
                    chain!(self, "let Some({ty}) = {let_expr}.ty");
                    self.maybe_path(ty);
                }
                self.expr(field!(let_expr.init));
            },
            ExprKind::Array(elements) => {
                bind!(self, elements);
                kind!("Array({elements})");
                self.slice(elements, |e| self.expr(e));
            },
            ExprKind::Call(func, args) => {
                bind!(self, func, args);
                kind!("Call({func}, {args})");
                self.expr(func);
                self.slice(args, |e| self.expr(e));
            },
            ExprKind::MethodCall(method_name, receiver, args, _) => {
                bind!(self, method_name, receiver, args);
                kind!("MethodCall({method_name}, {receiver}, {args}, _)");
                self.ident(field!(method_name.ident));
                self.expr(receiver);
                self.slice(args, |e| self.expr(e));
            },
            ExprKind::Tup(elements) => {
                bind!(self, elements);
                kind!("Tup({elements})");
                self.slice(elements, |e| self.expr(e));
            },
            ExprKind::Use(expr, _) => {
                bind!(self, expr);
                kind!("Use({expr})");
                self.expr(expr);
            },
            ExprKind::Binary(op, left, right) => {
                bind!(self, op, left, right);
                kind!("Binary({op}, {left}, {right})");
                chain!(self, "BinOpKind::{:?} == {op}.node", op.value.node);
                self.expr(left);
                self.expr(right);
            },
            ExprKind::Unary(op, inner) => {
                bind!(self, inner);
                kind!("Unary(UnOp::{op:?}, {inner})");
                self.expr(inner);
            },
            ExprKind::Lit(lit) => {
                bind!(self, lit);
                kind!("Lit(ref {lit})");
                self.lit(lit);
            },
            ExprKind::Cast(expr, cast_ty) => {
                bind!(self, expr, cast_ty);
                kind!("Cast({expr}, {cast_ty})");
                self.maybe_path(cast_ty);
                self.expr(expr);
            },
            ExprKind::Type(expr, _ty) => {
                bind!(self, expr);
                kind!("Type({expr}, _)");
                self.expr(expr);
            },
            ExprKind::Loop(body, label, des, _) => {
                bind!(self, body);
                opt_bind!(self, label);
                kind!("Loop({body}, {label}, LoopSource::{des:?}, _)");
                self.block(body);
                label.if_some(|l| self.ident(field!(l.ident)));
            },
            ExprKind::If(cond, then, else_expr) => {
                bind!(self, cond, then);
                opt_bind!(self, else_expr);
                kind!("If({cond}, {then}, {else_expr})");
                self.expr(cond);
                self.expr(then);
                else_expr.if_some(|e| self.expr(e));
            },
            ExprKind::Match(scrutinee, arms, des) => {
                bind!(self, scrutinee, arms);
                kind!("Match({scrutinee}, {arms}, MatchSource::{des:?})");
                self.expr(scrutinee);
                self.slice(arms, |arm| self.arm(arm));
            },
            ExprKind::Closure(&Closure {
                capture_clause,
                fn_decl,
                body: body_id,
                kind,
                ..
            }) => {
                let capture_clause = match capture_clause {
                    CaptureBy::Value { .. } => "Value { .. }",
                    CaptureBy::Use { .. } => "Use { .. }",
                    CaptureBy::Ref => "Ref",
                };

                let closure_kind = match kind {
                    ClosureKind::Closure => "ClosureKind::Closure".to_string(),
                    ClosureKind::Coroutine(coroutine_kind) => match coroutine_kind {
                        CoroutineKind::Desugared(desugaring, source) => format!(
                            "ClosureKind::Coroutine(CoroutineKind::Desugared(CoroutineDesugaring::{desugaring:?}, CoroutineSource::{source:?}))"
                        ),
                        CoroutineKind::Coroutine(movability) => {
                            format!("ClosureKind::Coroutine(CoroutineKind::Coroutine(Movability::{movability:?})")
                        },
                    },
                    ClosureKind::CoroutineClosure(desugaring) => {
                        format!("ClosureKind::CoroutineClosure(CoroutineDesugaring::{desugaring:?})")
                    },
                };

                let ret_ty = match fn_decl.output {
                    FnRetTy::DefaultReturn(_) => "FnRetTy::DefaultReturn(_)",
                    FnRetTy::Return(_) => "FnRetTy::Return(_ty)",
                };

                bind!(self, fn_decl, body_id);
                kind!(
                    "Closure {{ capture_clause: CaptureBy::{capture_clause}, fn_decl: {fn_decl}, body: {body_id}, closure_kind: {closure_kind}, .. }}"
                );
                chain!(self, "let {ret_ty} = {fn_decl}.output");
                self.body(body_id);
            },
            ExprKind::Yield(sub, source) => {
                bind!(self, sub);
                kind!("Yield(sub, YieldSource::{source:?})");
                self.expr(sub);
            },
            ExprKind::Block(block, label) => {
                bind!(self, block);
                opt_bind!(self, label);
                kind!("Block({block}, {label})");
                self.block(block);
                label.if_some(|l| self.ident(field!(l.ident)));
            },
            ExprKind::Assign(target, value, _) => {
                bind!(self, target, value);
                kind!("Assign({target}, {value}, _span)");
                self.expr(target);
                self.expr(value);
            },
            ExprKind::AssignOp(op, target, value) => {
                bind!(self, op, target, value);
                kind!("AssignOp({op}, {target}, {value})");
                chain!(self, "BinOpKind::{:?} == {op}.node", op.value.node);
                self.expr(target);
                self.expr(value);
            },
            ExprKind::Field(object, field_name) => {
                bind!(self, object, field_name);
                kind!("Field({object}, {field_name})");
                self.ident(field_name);
                self.expr(object);
            },
            ExprKind::Index(object, index, _) => {
                bind!(self, object, index);
                kind!("Index({object}, {index})");
                self.expr(object);
                self.expr(index);
            },
            ExprKind::Path(_) => {
                self.maybe_path(expr);
            },
            ExprKind::AddrOf(kind, mutability, inner) => {
                bind!(self, inner);
                kind!("AddrOf(BorrowKind::{kind:?}, Mutability::{mutability:?}, {inner})");
                self.expr(inner);
            },
            ExprKind::Break(destination, value) => {
                bind!(self, destination);
                opt_bind!(self, value);
                kind!("Break({destination}, {value})");
                self.destination(destination);
                value.if_some(|e| self.expr(e));
            },
            ExprKind::Continue(destination) => {
                bind!(self, destination);
                kind!("Continue({destination})");
                self.destination(destination);
            },
            ExprKind::Ret(value) => {
                opt_bind!(self, value);
                kind!("Ret({value})");
                value.if_some(|e| self.expr(e));
            },
            ExprKind::Become(value) => {
                bind!(self, value);
                kind!("Become({value})");
                self.expr(value);
            },
            ExprKind::InlineAsm(_) => {
                kind!("InlineAsm(_)");
                out!("// unimplemented: `ExprKind::InlineAsm` is not further destructured at the moment");
            },
            ExprKind::OffsetOf(container, ref fields) => {
                bind!(self, container, fields);
                kind!("OffsetOf({container}, {fields})");
            },
            ExprKind::Struct(qpath, fields, base) => {
                bind!(self, qpath, fields);
                let base = OptionPat::new(match base {
                    StructTailExpr::Base(base) => Some(self.bind("base", base)),
                    StructTailExpr::None | StructTailExpr::DefaultFields(_) => None,
                });
                kind!("Struct({qpath}, {fields}, {base})");
                self.qpath(qpath, &expr.name, expr.value.hir_id);
                self.slice(fields, |field| {
                    self.ident(field!(field.ident));
                    self.expr(field!(field.expr));
                });
                base.if_some(|e| self.expr(e));
            },
            ExprKind::ConstBlock(_) => kind!("ConstBlock(_)"),
            ExprKind::Repeat(value, length) => {
                bind!(self, value, length);
                kind!("Repeat({value}, {length})");
                self.expr(value);
                self.const_arg(length);
            },
            ExprKind::Err(_) => kind!("Err(_)"),
            ExprKind::DropTemps(expr) => {
                bind!(self, expr);
                kind!("DropTemps({expr})");
                self.expr(expr);
            },
            ExprKind::UnsafeBinderCast(..) => {
                unimplemented!("unsafe binders are not implemented yet");
            },
        }
    }

    fn block(&self, block: &Binding<&hir::Block<'_>>) {
        self.slice(field!(block.stmts), |stmt| self.stmt(stmt));
        self.option(field!(block.expr), "trailing_expr", |expr| {
            self.expr(expr);
        });
    }

    fn body(&self, body_id: &Binding<hir::BodyId>) {
        let expr = self.cx.tcx.hir_body(body_id.value).value;
        bind!(self, expr);
        chain!(self, "{expr} = &cx.tcx.hir_body({body_id}).value");
        self.expr(expr);
    }

    fn pat_expr(&self, lit: &Binding<&hir::PatExpr<'_>>, pat: &Binding<&hir::Pat<'_>>) {
        let kind = |kind| chain!(self, "let PatExprKind::{kind} = {lit}.kind");
        macro_rules! kind {
            ($($t:tt)*) => (kind(format_args!($($t)*)));
        }
        match lit.value.kind {
            PatExprKind::Lit { lit, negated } => {
                bind!(self, lit);
                bind!(self, negated);
                kind!("Lit {{ ref {lit}, {negated} }}");
                self.lit(lit);
            },
            PatExprKind::ConstBlock(_) => kind!("ConstBlock(_)"),
            PatExprKind::Path(_) => self.maybe_path(pat),
        }
    }

    fn pat(&self, pat: &Binding<&hir::Pat<'_>>) {
        let kind = |kind| chain!(self, "let PatKind::{kind} = {pat}.kind");
        macro_rules! kind {
            ($($t:tt)*) => (kind(format_args!($($t)*)));
        }

        match pat.value.kind {
            PatKind::Missing => unreachable!(),
            PatKind::Wild => kind!("Wild"),
            PatKind::Never => kind!("Never"),
            PatKind::Binding(ann, _, name, sub) => {
                bind!(self, name);
                opt_bind!(self, sub);
                let ann = match ann {
                    BindingMode::NONE => "NONE",
                    BindingMode::REF => "REF",
                    BindingMode::MUT => "MUT",
                    BindingMode::REF_MUT => "REF_MUT",
                    BindingMode::MUT_REF => "MUT_REF",
                    BindingMode::MUT_REF_MUT => "MUT_REF_MUT",
                };
                kind!("Binding(BindingMode::{ann}, _, {name}, {sub})");
                self.ident(name);
                sub.if_some(|p| self.pat(p));
            },
            PatKind::Struct(ref qpath, fields, etc) => {
                let ignore = etc.is_some();
                bind!(self, qpath, fields);
                kind!("Struct(ref {qpath}, {fields}, {ignore})");
                self.qpath(qpath, &pat.name, pat.value.hir_id);
                self.slice(fields, |field| {
                    self.ident(field!(field.ident));
                    self.pat(field!(field.pat));
                });
            },
            PatKind::Or(fields) => {
                bind!(self, fields);
                kind!("Or({fields})");
                self.slice(fields, |pat| self.pat(pat));
            },
            PatKind::TupleStruct(ref qpath, fields, skip_pos) => {
                bind!(self, qpath, fields);
                kind!("TupleStruct(ref {qpath}, {fields}, {skip_pos:?})");
                self.qpath(qpath, &pat.name, pat.value.hir_id);
                self.slice(fields, |pat| self.pat(pat));
            },
            PatKind::Tuple(fields, skip_pos) => {
                bind!(self, fields);
                kind!("Tuple({fields}, {skip_pos:?})");
                self.slice(fields, |field| self.pat(field));
            },
            PatKind::Box(pat) => {
                bind!(self, pat);
                kind!("Box({pat})");
                self.pat(pat);
            },
            PatKind::Deref(pat) => {
                bind!(self, pat);
                kind!("Deref({pat})");
                self.pat(pat);
            },
            PatKind::Ref(pat, muta) => {
                bind!(self, pat);
                kind!("Ref({pat}, Mutability::{muta:?})");
                self.pat(pat);
            },
            PatKind::Guard(pat, cond) => {
                bind!(self, pat, cond);
                kind!("Guard({pat}, {cond})");
                self.pat(pat);
                self.expr(cond);
            },
            PatKind::Expr(lit_expr) => {
                bind!(self, lit_expr);
                kind!("Expr({lit_expr})");
                self.pat_expr(lit_expr, pat);
            },
            PatKind::Range(start, end, end_kind) => {
                opt_bind!(self, start, end);
                kind!("Range({start}, {end}, RangeEnd::{end_kind:?})");
                start.if_some(|e| self.pat_expr(e, pat));
                end.if_some(|e| self.pat_expr(e, pat));
            },
            PatKind::Slice(start, middle, end) => {
                bind!(self, start, end);
                opt_bind!(self, middle);
                kind!("Slice({start}, {middle}, {end})");
                middle.if_some(|p| self.pat(p));
                self.slice(start, |pat| self.pat(pat));
                self.slice(end, |pat| self.pat(pat));
            },
            PatKind::Err(_) => kind!("Err"),
        }
    }

    fn stmt(&self, stmt: &Binding<&hir::Stmt<'_>>) {
        let kind = |kind| chain!(self, "let StmtKind::{kind} = {stmt}.kind");
        macro_rules! kind {
            ($($t:tt)*) => (kind(format_args!($($t)*)));
        }

        match stmt.value.kind {
            StmtKind::Let(local) => {
                bind!(self, local);
                kind!("Let({local})");
                self.option(field!(local.init), "init", |init| {
                    self.expr(init);
                });
                self.pat(field!(local.pat));
            },
            StmtKind::Item(_) => kind!("Item(item_id)"),
            StmtKind::Expr(e) => {
                bind!(self, e);
                kind!("Expr({e})");
                self.expr(e);
            },
            StmtKind::Semi(e) => {
                bind!(self, e);
                kind!("Semi({e})");
                self.expr(e);
            },
        }
    }
}

fn has_attr(cx: &LateContext<'_>, hir_id: HirId) -> bool {
    let attrs = cx.tcx.hir_attrs(hir_id);
    get_attr(cx.sess(), attrs, sym::author).count() > 0
}
