1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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
}