Skip to main content

frs/cmds/rsl/rules/
misordered_item_group.rs

1//! Misordered-item-group rule for `frs rsl`.
2
3use 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            // Keep unknown macro invocations transparent here to preserve the original group rule.
53            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}