cargo-gears-lints 0.0.4

Dylint lint collection for cargo-gears architectural rules
extern crate rustc_ast;

use crate::lint_utils::is_in_domain_path;
use rustc_ast::{Item, ItemKind, VisibilityKind};
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};

dylint_linting::declare_pre_expansion_lint! {
    /// DE0309: Domain Structs Must Have `#[domain_model]` Attribute
    ///
    /// Struct and enum types in the domain layer that are visible beyond their own
    /// module (`pub`, `pub(crate)`, `pub(super)`, `pub(in ...)`) MUST have the
    /// `#[domain_model]` attribute to ensure compile-time validation of DDD boundaries.
    ///
    /// Strictly module-private types (no `pub` keyword) are exempt: they are pure
    /// implementation details that never cross a layer boundary, and their fields
    /// are still guarded against infrastructure leakage by `DE0301_NO_INFRA_IN_DOMAIN`
    /// and `DE0308_NO_HTTP_IN_DOMAIN`, which check every domain struct/enum regardless
    /// of this attribute. This keeps small technical helpers (e.g. a `HashMap`-key
    /// newtype) from needing either a spurious `#[domain_model]` or an `#[allow(...)]`.
    ///
    /// ### Example: Bad
    ///
    /// ```rust,ignore
    /// // src/domain/user.rs
    /// pub struct User {           // Missing #[domain_model]
    ///     pub id: Uuid,
    ///     pub email: String,
    /// }
    /// ```
    ///
    /// ### Example: Good
    ///
    /// ```rust,ignore
    /// // src/domain/user.rs
    /// use toolkit_macros::domain_model;
    ///
    /// #[domain_model]
    /// pub struct User {
    ///     pub id: Uuid,
    ///     pub email: String,
    /// }
    /// ```
    #[doc = include_str!("de0309_must_have_domain_model/README.md")]
    pub DE0309_MUST_HAVE_DOMAIN_MODEL,
    Deny,
    "domain types must have #[domain_model] attribute for DDD boundary enforcement (DE0309)"
}

impl EarlyLintPass for De0309MustHaveDomainModel {
    fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
        check_domain_model_attribute(cx, item);
    }
}

fn check_domain_model_attribute(cx: &EarlyContext<'_>, item: &Item) {
    // Only check structs and enums
    if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) {
        return;
    }

    // Only check items in domain path
    if !is_in_domain_path(cx.sess().source_map(), item.span) {
        return;
    }

    // Exempt strictly module-private types (no `pub` keyword). They never cross a
    // layer boundary, and their fields are still checked for infra leakage by
    // DE0301/DE0308 regardless of this attribute. `pub`/`pub(crate)`/`pub(super)`/
    // `pub(in ...)` remain subject to the requirement.
    if matches!(item.vis.kind, VisibilityKind::Inherited) {
        return;
    }

    // Check if the item has #[domain_model] attribute
    if has_domain_model_attribute(item) {
        return;
    }

    // Get item kind and name for error message
    let (item_keyword, item_name) = match &item.kind {
        ItemKind::Struct(ident, ..) => ("struct", ident.name.as_str()),
        ItemKind::Enum(ident, ..) => ("enum", ident.name.as_str()),
        _ => return,
    };

    cx.span_lint(DE0309_MUST_HAVE_DOMAIN_MODEL, item.span, |diag| {
        diag.primary_message(format!(
            "domain type `{item_name}` is missing required #[domain_model] attribute (DE0309)"
        ));
        diag.help(format!(
            "add #[domain_model] attribute to enforce DDD boundaries at compile time: \
             use toolkit_macros::domain_model; #[domain_model] pub {item_keyword} ..."
        ));
    });
}

/// Check if an item has the `#[domain_model]` or `#[toolkit::domain_model]` attribute.
fn has_domain_model_attribute(item: &Item) -> bool {
    for attr in &item.attrs {
        if let rustc_ast::AttrKind::Normal(attr_item) = &attr.kind {
            let path = &attr_item.item.path;
            let segments: Vec<&str> = path
                .segments
                .iter()
                .map(|s| s.ident.name.as_str())
                .collect();

            // Match: domain_model, toolkit::domain_model, toolkit_macros::domain_model
            if segments.last() == Some(&"domain_model") {
                return true;
            }
        }
    }
    false
}