cargo-gears-lints 0.0.1

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

use rustc_hir::{Item, ItemKind};
use rustc_lint::{LateContext, LateLintPass, LintContext};

dylint_linting::declare_late_lint! {
    /// DE0202: DTOs not referenced outside API
    ///
    /// DTO types must not be imported by contract (SDK), domain, or infra modules.
    /// DTOs are API layer implementation details.
    #[doc = include_str!("../../docs/de02_api_layer/de0202_dtos_not_referenced_outside_api/README.md")]
    pub DE0202_DTOS_NOT_REFERENCED_OUTSIDE_API,
    Deny,
    "DTO types should not be imported outside of api layer (DE0202)"
}

impl<'tcx> LateLintPass<'tcx> for De0202DtosNotReferencedOutsideApi {
    fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
        // Only check use statements
        let ItemKind::Use(path, _) = &item.kind else {
            return;
        };

        // Check if we're in a forbidden module (contract/SDK, domain, infra)
        let sm = cx.sess().source_map();
        let span = cx.tcx.def_span(item.owner_id.def_id);

        let in_forbidden = crate::lint_utils::is_in_contract_path(sm, span)
            || crate::lint_utils::is_in_domain_path(sm, span)
            || crate::lint_utils::is_in_infra_path(sm, span);
        if !in_forbidden {
            return;
        }

        // Check if the import path references api::rest::dto
        let path_str = path_to_string(path);

        // Only check imports from api::rest::dto or api::rest
        if !path_str.contains("api::rest::dto") && !path_str.contains("api::rest") {
            return;
        }

        // Check if importing a DTO type
        let segments: Vec<&str> = path_str.split("::").collect();
        if let Some(last) = segments.last() {
            let is_dto = last.ends_with("Dto")
                || last.ends_with("Request")
                || last.ends_with("Response")
                || last.ends_with("Query");

            if is_dto {
                let module_type = if crate::lint_utils::is_in_contract_path(sm, span) {
                    "contract"
                } else if crate::lint_utils::is_in_domain_path(sm, span) {
                    "domain"
                } else {
                    "infra"
                };

                cx.span_lint(DE0202_DTOS_NOT_REFERENCED_OUTSIDE_API, item.span, |diag| {
                    diag.primary_message(format!(
                        "{} module imports DTO type `{}` from api layer (DE0202)",
                        module_type, last
                    ));
                    diag.help("DTOs are API layer details; use domain types instead");
                });
            }
        }
    }
}

fn path_to_string(path: &rustc_hir::UsePath<'_>) -> String {
    path.segments
        .iter()
        .map(|seg| seg.ident.name.as_str())
        .collect::<Vec<_>>()
        .join("::")
}