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
extern crate rustc_ast;
use rustc_ast::{Item, ItemKind};
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
use crate::lint_utils::is_in_api_rest_folder;
dylint_linting::declare_pre_expansion_lint! {
/// DE0203: DTOs Must Use api_dto Macro
///
/// All DTO types in `api/rest` MUST use the `#[cf_gears_toolkit_macros::api_dto(...)]` macro.
/// The macro ensures consistent serialization behavior by automatically adding
/// serde derives, ToSchema, and snake_case renaming.
///
/// ### Example: Bad
///
/// ```rust,ignore
/// // src/api/rest/dto.rs
/// #[derive(Debug, Clone, Serialize, Deserialize)] // ❌ Manual derives instead of api_dto
/// pub struct UserDto {
/// pub id: String,
/// }
/// ```
///
/// ### Example: Good
///
/// ```rust,ignore
/// // src/api/rest/dto.rs
/// #[cf_gears_toolkit_macros::api_dto(request, response)] // ✅ Uses api_dto macro
/// pub struct UserDto {
/// pub id: String,
/// }
/// ```
#[doc = include_str!("de0203_dtos_must_use_api_dto/README.md")]
pub DE0203_DTOS_MUST_USE_API_DTO,
Deny,
"DTO types must use the api_dto macro (DE0203)"
}
impl EarlyLintPass for De0203DtosMustUseApiDto {
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
check_dto_uses_api_dto(cx, item);
}
}
fn check_dto_uses_api_dto(cx: &EarlyContext<'_>, item: &Item) {
// Only check structs and enums
if !matches!(item.kind, ItemKind::Struct(..) | ItemKind::Enum(..)) {
return;
}
// Only check items in api/rest folder
if !is_in_api_rest_folder(cx.sess().source_map(), item.span) {
return;
}
// Check if the type name ends with "Dto" suffix (case-insensitive)
let item_name = match &item.kind {
ItemKind::Struct(ident, _, _) => ident.name.as_str(),
ItemKind::Enum(ident, _, _) => ident.name.as_str(),
_ => return,
};
let item_name_lower = item_name.to_lowercase();
if !item_name_lower.ends_with("dto") {
return;
}
// Check for api_dto macro
if crate::lint_utils::has_api_dto_attribute(item) {
return;
}
// Report missing api_dto macro
cx.span_lint(DE0203_DTOS_MUST_USE_API_DTO, item.span, |diag| {
diag.primary_message("api/rest DTO type must use the api_dto macro (DE0203)");
diag.help("Use #[cf_gears_toolkit_macros::api_dto(request)] for request DTOs, #[cf_gears_toolkit_macros::api_dto(response)] for response DTOs, or #[cf_gears_toolkit_macros::api_dto(request, response)] for both");
});
}