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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
extern crate rustc_ast;
extern crate rustc_span;
use rustc_ast::{Item, ItemKind};
use rustc_lint::{EarlyContext, EarlyLintPass, LintContext};
use rustc_span::Span;
dylint_linting::declare_early_lint! {
/// ### What it does
///
/// Enforces that Client and PluginClient traits in non-system modules have version suffixes (V1, V2, etc.).
///
/// # Why is this bad?
///
/// Non-system modules require explicit versioning for their public API contracts to enable
/// parallel versions and clear upgrade paths. System modules are exempt because they follow
/// different versioning rules managed at the platform level.
///
/// # Scope
/// - **Applies to**: All SDK crates in `modules/*` (except `modules/system/*`) and `examples/*`
/// - **Does NOT apply to**: System modules only (`modules/system/*`)
///
/// # Example
/// ```rust,ignore
/// // Bad (in modules/simple_user_settings or examples/*)
/// pub trait UsersInfoClient: Send + Sync {
/// async fn get_user(&self) -> Result<User, Error>;
/// }
///
/// // Good (in modules/simple_user_settings or examples/*)
/// pub trait UsersInfoClientV1: Send + Sync {
/// async fn get_user(&self) -> Result<User, Error>;
/// }
///
/// // OK (in modules/system/* - exempt from versioning)
/// pub trait TypesRegistryClient: Send + Sync {
/// async fn register(&self) -> Result<(), Error>;
/// }
/// ```
pub DE0504_CLIENT_VERSIONING,
Deny,
"Client and PluginClient traits in non-system modules must have version suffixes (V1, V2, etc.) (DE0504)"
}
impl EarlyLintPass for De0504ClientVersioning {
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &Item) {
// Only check trait definitions
let ItemKind::Trait(trait_data) = &item.kind else {
return;
};
// Only apply this lint to *-sdk crates or UI test examples
if !crate::lint_utils::is_in_sdk_crate(cx, item.span) {
return;
}
// EXEMPTION: Skip system modules (modules/system/*) from versioning requirements.
// UI tests always run for testing purposes even if they simulate system modules.
if is_system_module(cx, item.span) && !is_ui_test(cx, item.span) {
return;
}
let trait_name = trait_data.ident.name.as_str();
if trait_name.is_empty() {
return;
}
let version = crate::lint_utils::parse_version_suffix(trait_name);
// Only match traits whose base name ends with "Client" to avoid false positives
// on helper traits like ClientEventHandler, ClientConfiguration, etc.
if !version.base.ends_with("Client") {
return;
}
// If it has a valid version suffix (V1, V2, etc.), it's fine
if version.has_valid_version() {
return;
}
emit_lint(cx, item.span, trait_name, &version);
}
}
fn is_ui_test(cx: &EarlyContext<'_>, span: Span) -> bool {
let Some(file_path) = crate::lint_utils::filename_str(cx.sess().source_map(), span) else {
return false;
};
crate::lint_utils::is_temp_path(&file_path)
}
/// Checks if the file is part of a system module (modules/system/*).
fn is_system_module(cx: &EarlyContext<'_>, span: Span) -> bool {
let Some(file_path) = crate::lint_utils::filename_str(cx.sess().source_map(), span) else {
return false;
};
file_path.contains("modules/system/") || file_path.contains("modules\\system\\")
}
fn emit_lint(
cx: &EarlyContext<'_>,
span: Span,
trait_name: &str,
version: &crate::lint_utils::VersionParts<'_>,
) {
let suggestion =
if version.has_malformed_version() && !version.malformed_digits.starts_with('0') {
// Trailing digits without V prefix: suggest inserting V
// e.g., UsersInfoClient2 -> UsersInfoClientV2
format!("{}V{}", version.base, version.malformed_digits)
} else {
// No version, bare V, V0, or leading-zero digits: suggest appending V1 to base
format!("{}V1", version.base)
};
cx.span_lint(DE0504_CLIENT_VERSIONING, span, |diag| {
diag.primary_message(format!(
"Client trait `{trait_name}` in non-system module must have a version suffix (DE0504)"
));
diag.help(format!(
"rename trait to `{suggestion}` to indicate API version"
));
});
}
#[cfg(test)]
mod tests {
// NOTE: Positive-case testing (lint fires on bad code) is covered by UI tests in ui/
// (non_system_missing_version.rs, invalid_version_suffix.rs, generic_parameters.rs).
// Integration tests in tests/system_module_exemption.rs verify the system module
// exemption works with real crate paths, which cannot be tested through UI tests.
// --- Unit tests for crate::lint_utils::parse_version_suffix ---
// Placed here because lint_utils can't run unit tests directly (rustc_private linking).
fn assert_version(
name: &str,
expected_base: &str,
expected_suffix: &str,
expected_malformed: &str,
) {
let v = crate::lint_utils::parse_version_suffix(name);
assert_eq!(
v.base, expected_base,
"parse_version_suffix({name:?}): base mismatch"
);
assert_eq!(
v.version_suffix, expected_suffix,
"parse_version_suffix({name:?}): version_suffix mismatch"
);
assert_eq!(
v.malformed_digits, expected_malformed,
"parse_version_suffix({name:?}): malformed_digits mismatch"
);
}
#[test]
fn test_parse_version_suffix_empty_and_single_char() {
assert_version("", "", "", "");
// Single "V" is just a name, not a bare-V suffix (requires len > 1)
assert_version("V", "V", "", "");
assert_version("A", "A", "", "");
assert_version("1", "", "", "1");
}
#[test]
fn test_parse_version_suffix_valid_versions() {
assert_version("FooClientV1", "FooClient", "V1", "");
assert_version("FooClientV2", "FooClient", "V2", "");
assert_version("FooClientV10", "FooClient", "V10", "");
assert_version("FooClientV99", "FooClient", "V99", "");
assert_version("V1", "", "V1", "");
}
#[test]
fn test_parse_version_suffix_rejected_versions() {
// V0: version zero is invalid
assert_version("FooClientV0", "FooClient", "", "");
// V00: leading zero
assert_version("FooClientV00", "FooClient", "", "");
// V01: leading zero
assert_version("FooClientV01", "FooClient", "", "");
// V0 standalone
assert_version("V0", "", "", "");
}
#[test]
fn test_parse_version_suffix_bare_v() {
assert_version("FooClientV", "FooClient", "", "");
assert_version("VV", "V", "", "");
}
#[test]
fn test_parse_version_suffix_malformed_digits() {
assert_version("FooClient2", "FooClient", "", "2");
assert_version("FooClient123", "FooClient", "", "123");
assert_version("Client1", "Client", "", "1");
}
#[test]
fn test_parse_version_suffix_no_suffix() {
assert_version("FooClient", "FooClient", "", "");
assert_version("ThrPluginApi", "ThrPluginApi", "", "");
assert_version("SomeTraitName", "SomeTraitName", "", "");
}
#[test]
fn test_version_parts_helpers() {
let v = crate::lint_utils::parse_version_suffix("FooClientV1");
assert!(v.has_valid_version());
assert!(!v.has_malformed_version());
let v = crate::lint_utils::parse_version_suffix("FooClient2");
assert!(!v.has_valid_version());
assert!(v.has_malformed_version());
let v = crate::lint_utils::parse_version_suffix("FooClient");
assert!(!v.has_valid_version());
assert!(!v.has_malformed_version());
let v = crate::lint_utils::parse_version_suffix("FooClientV0");
assert!(!v.has_valid_version());
assert!(!v.has_malformed_version());
let v = crate::lint_utils::parse_version_suffix("FooClientV");
assert!(!v.has_valid_version());
assert!(!v.has_malformed_version());
}
}