1use std::path::Path;
4
5use proc_macro2::Span;
6
7use super::common::Location;
8use crate::cmds::rsl::ast::ItemGroup;
9use crate::cmds::rsl::ast::ItemKind;
10use crate::cmds::rsl::engine::FileContext;
11use crate::cmds::rsl::rules::TypedRule;
12use crate::cmds::rsl::rules::TypedRuleViolation;
13
14pub struct MisorderedItemGroupRule {
15 group_order: [ItemGroup; 7],
16}
17
18impl MisorderedItemGroupRule {
19 pub(super) fn new(group_order: Option<[ItemGroup; 7]>) -> Self {
20 Self {
21 group_order: group_order.unwrap_or([
22 ItemGroup::ExternCrate,
23 ItemGroup::Use,
24 ItemGroup::Modules,
25 ItemGroup::GlobalAsm,
26 ItemGroup::Constants,
27 ItemGroup::Aliases,
28 ItemGroup::Items,
29 ]),
30 }
31 }
32
33 fn group_rank(&self, group: ItemGroup) -> usize {
34 self.group_order
35 .iter()
36 .position(|expected| *expected == group)
37 .unwrap_or(self.group_order.len())
38 }
39}
40
41impl TypedRule for MisorderedItemGroupRule {
42 type Violation = MisorderedItemGroupViolation;
43
44 fn code() -> &'static str {
45 "misordered_item_group"
46 }
47
48 fn check(&self, ctx: &FileContext<'_>) -> Vec<Self::Violation> {
49 let mut violations = Vec::new();
50
51 for items in &ctx.module_item_lists {
52 let mut previous_group: Option<ItemGroup> = None;
54
55 for (idx, module_item) in items.iter().enumerate() {
56 let metadata = module_item.metadata();
57 let Some(classified) = metadata.classified() else {
58 continue;
59 };
60 if metadata.is_test_module() {
61 if idx != items.len().saturating_sub(1) {
62 violations.push(MisorderedItemGroupViolation::new(
63 ctx.path,
64 classified.span,
65 ItemGroup::Items,
66 classified.kind,
67 ));
68 }
69 continue;
70 }
71
72 let actual_group = classified.kind.group();
73 if let Some(expected_group) = previous_group
74 && self.group_rank(actual_group) < self.group_rank(expected_group)
75 {
76 violations.push(MisorderedItemGroupViolation::new(
77 ctx.path,
78 classified.span,
79 expected_group,
80 classified.kind,
81 ));
82 }
83 previous_group = Some(actual_group);
84 }
85 }
86
87 violations
88 }
89}
90
91#[derive(Debug)]
92#[cfg_attr(test, derive(Eq, PartialEq))]
93pub struct MisorderedItemGroupViolation {
94 pub location: Location,
95 pub details: MisorderedItemGroupDetails,
96}
97
98impl MisorderedItemGroupViolation {
99 fn new(path: &Path, span: Span, expected_group: ItemGroup, item: ItemKind) -> Self {
100 Self {
101 location: Location::from_span(path, span),
102 details: MisorderedItemGroupDetails { expected_group, item },
103 }
104 }
105}
106
107impl TypedRuleViolation for MisorderedItemGroupViolation {
108 type Rule = MisorderedItemGroupRule;
109}
110
111#[derive(Debug)]
112#[cfg_attr(test, derive(Eq, PartialEq))]
113pub struct MisorderedItemGroupDetails {
114 pub expected_group: ItemGroup,
115 pub item: ItemKind,
116}
117
118#[cfg(test)]
119mod tests {
120 use std::path::PathBuf;
121
122 use test_that::prelude::*;
123
124 use super::MisorderedItemGroupDetails;
125 use super::MisorderedItemGroupRule;
126 use super::MisorderedItemGroupViolation;
127 use crate::cmds::rsl::ast::ItemGroup;
128 use crate::cmds::rsl::rules::TypedRule;
129 use crate::cmds::rsl::rules::common::Location;
130
131 #[test]
132 fn test_classify_item_when_each_group_is_present_returns_expected_groups() {
133 assert_that!(
134 groups(
135 r#"
136 extern crate alloc;
137 #[cfg(feature = "imports")]
138 use alloc::vec::Vec;
139 extern "C" {}
140 #[rsl_test]
141 mod child {}
142 global_asm!("");
143 const VALUE: usize = 1;
144 static OTHER: usize = 2;
145 type Alias = usize;
146 macro_rules! declared {}
147 enum Kind {}
148 struct Data;
149 union Storage { value: usize }
150 trait Behavior {}
151 trait AliasTrait = Behavior;
152 impl Data {}
153 fn run() {}
154 opaque!();
155 "#
156 ),
157 eq(vec![
158 Some(ItemGroup::ExternCrate),
159 Some(ItemGroup::Use),
160 Some(ItemGroup::Modules),
161 Some(ItemGroup::Modules),
162 Some(ItemGroup::GlobalAsm),
163 Some(ItemGroup::Constants),
164 Some(ItemGroup::Constants),
165 Some(ItemGroup::Aliases),
166 Some(ItemGroup::Items),
167 Some(ItemGroup::Items),
168 Some(ItemGroup::Items),
169 Some(ItemGroup::Items),
170 Some(ItemGroup::Items),
171 Some(ItemGroup::Items),
172 Some(ItemGroup::Items),
173 Some(ItemGroup::Items),
174 None,
175 ])
176 );
177 }
178
179 #[test]
180 fn test_misordered_item_group_rule_check_when_group_rank_decreases_reports_offending_item() {
181 let syntax = syn::parse_file(
182 r"
183 const VALUE: usize = 1;
184 fn run() {}
185 use std::fmt;
186 ",
187 )
188 .unwrap();
189
190 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
191
192 assert_that!(
193 result,
194 eq(vec![MisorderedItemGroupViolation {
195 location: Location::new(PathBuf::from("test.rs"), 4, 13),
196 details: MisorderedItemGroupDetails {
197 expected_group: ItemGroup::Items,
198 item: crate::cmds::rsl::ast::ItemKind::Use,
199 },
200 }])
201 );
202 }
203
204 #[test]
205 fn test_misordered_item_group_rule_check_when_same_group_repeats_preserves_clean_order() {
206 let syntax = syn::parse_file(
207 r"
208 const FIRST: usize = 1;
209 static SECOND: usize = 2;
210 fn run() {}
211 fn stop() {}
212 ",
213 )
214 .unwrap();
215
216 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
217
218 assert_that!(result, is_empty());
219 }
220
221 #[test]
222 fn test_misordered_item_group_rule_check_when_cfg_test_tests_module_is_last_returns_no_violations() {
223 let syntax = syn::parse_file(
224 r"
225 fn run() {}
226
227 #[cfg(test)]
228 mod tests {}
229 ",
230 )
231 .unwrap();
232
233 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
234
235 assert_that!(result, is_empty());
236 }
237
238 #[test]
239 fn test_misordered_item_group_rule_check_when_cfg_test_tests_module_is_not_last_reports_violation() {
240 let syntax = syn::parse_file(
241 r"
242 #[cfg(test)]
243 mod tests {}
244
245 fn run() {}
246 ",
247 )
248 .unwrap();
249
250 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
251
252 assert_that!(
253 result,
254 eq(vec![MisorderedItemGroupViolation {
255 location: Location::new(PathBuf::from("test.rs"), 3, 13),
256 details: MisorderedItemGroupDetails {
257 expected_group: ItemGroup::Items,
258 item: crate::cmds::rsl::ast::ItemKind::Mod,
259 },
260 }])
261 );
262 }
263
264 #[test]
265 fn test_misordered_item_group_rule_check_when_opaque_macro_is_between_items_ignores_macro_barrier() {
266 let syntax = syn::parse_file(
267 r"
268 const VALUE: usize = 1;
269 opaque!();
270 fn run() {}
271 ",
272 )
273 .unwrap();
274
275 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
276
277 assert_that!(result, is_empty());
278 }
279
280 #[test]
281 fn test_misordered_item_group_rule_check_when_explicit_macro_definition_precedes_constant_reports_violation() {
282 let syntax = syn::parse_file(
283 r"
284 macro_rules! declared {}
285 const VALUE: usize = 1;
286 ",
287 )
288 .unwrap();
289
290 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
291
292 assert_that!(
293 result,
294 eq(vec![MisorderedItemGroupViolation {
295 location: Location::new(PathBuf::from("test.rs"), 3, 13),
296 details: MisorderedItemGroupDetails {
297 expected_group: ItemGroup::Items,
298 item: crate::cmds::rsl::ast::ItemKind::Const,
299 },
300 }])
301 );
302 }
303
304 #[test]
305 fn test_classify_item_when_declarative_macro_uses_macro_keyword_returns_items_group() {
306 assert_that!(
307 groups(
308 r"
309 macro declared {}
310 "
311 ),
312 eq(vec![Some(ItemGroup::Items)])
313 );
314 }
315
316 #[test]
317 fn test_misordered_item_group_rule_check_when_inline_module_contains_violation_reports_nested_item() {
318 let syntax = syn::parse_file(
319 r"
320 mod child {
321 fn run() {}
322 const VALUE: usize = 1;
323 }
324 ",
325 )
326 .unwrap();
327
328 let result = MisorderedItemGroupRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
329
330 assert_that!(
331 result,
332 eq(vec![MisorderedItemGroupViolation {
333 location: Location::new(PathBuf::from("test.rs"), 4, 17),
334 details: MisorderedItemGroupDetails {
335 expected_group: ItemGroup::Items,
336 item: crate::cmds::rsl::ast::ItemKind::Const,
337 },
338 }])
339 );
340 }
341
342 fn groups(source: &str) -> Vec<Option<crate::cmds::rsl::ast::ItemGroup>> {
343 syn::parse_file(source)
344 .unwrap()
345 .items
346 .iter()
347 .map(crate::cmds::rsl::ast::classify_item)
348 .map(|item| item.map(|item| item.kind.group()))
349 .collect()
350 }
351}