Skip to main content

frs/cmds/rsl/rules/
misordered_visibility.rs

1//! Misordered-visibility rule for `frs rsl`.
2
3use std::path::Path;
4
5use proc_macro2::Span;
6use syn::Item;
7
8use super::common::Location;
9use crate::cmds::rsl::ast::VisibilityClass;
10use crate::cmds::rsl::engine::FileContext;
11use crate::cmds::rsl::rules::TypedRule;
12use crate::cmds::rsl::rules::TypedRuleViolation;
13
14pub struct MisorderedVisibilityRule {
15    visibility_order: [VisibilityClass; 4],
16}
17
18impl MisorderedVisibilityRule {
19    pub(super) fn new(visibility_order: Option<[VisibilityClass; 4]>) -> Self {
20        Self {
21            visibility_order: visibility_order.unwrap_or([
22                VisibilityClass::Public,
23                VisibilityClass::Crate,
24                VisibilityClass::Restricted,
25                VisibilityClass::Private,
26            ]),
27        }
28    }
29
30    fn check_visibility_order(
31        &self,
32        nodes: &[crate::cmds::rsl::ast::OrderNode],
33        path: &Path,
34        violations: &mut Vec<MisorderedVisibilityViolation>,
35    ) {
36        let mut current_group = None;
37        let mut highest_visibility: Option<&crate::cmds::rsl::ast::OrderNode> = None;
38
39        for node in nodes {
40            if node.group != current_group {
41                current_group = node.group;
42                highest_visibility = None;
43            }
44
45            let Some(visibility) = node.visibility else {
46                continue;
47            };
48
49            if let Some(previous) = highest_visibility
50                && self.visibility_rank(visibility)
51                    < self.visibility_rank(previous.visibility.unwrap_or(VisibilityClass::Private))
52            {
53                violations.push(MisorderedVisibilityViolation::new(
54                    path,
55                    node.span,
56                    previous.label.clone(),
57                    node.label.clone(),
58                ));
59            }
60
61            if highest_visibility.is_none_or(|previous| {
62                self.visibility_rank(visibility)
63                    > self.visibility_rank(previous.visibility.unwrap_or(VisibilityClass::Private))
64            }) {
65                highest_visibility = Some(node);
66            }
67        }
68    }
69
70    fn visibility_rank(&self, visibility: VisibilityClass) -> usize {
71        self.visibility_order
72            .iter()
73            .position(|expected| *expected == visibility)
74            .unwrap_or(self.visibility_order.len())
75    }
76}
77
78impl TypedRule for MisorderedVisibilityRule {
79    type Violation = MisorderedVisibilityViolation;
80
81    fn code() -> &'static str {
82        "misordered_visibility"
83    }
84
85    fn check(&self, ctx: &FileContext<'_>) -> Vec<Self::Violation> {
86        let mut violations = Vec::new();
87
88        for items in &ctx.module_item_lists {
89            let nodes = crate::cmds::rsl::ast::module_nodes(items);
90            let order_nodes: Vec<_> = nodes.iter().map(|node| node.order.clone()).collect();
91            self.check_visibility_order(&order_nodes, ctx.path, &mut violations);
92
93            for module_item in items.iter().rev() {
94                let item = module_item.item();
95                if let Item::Impl(item_impl) = item
96                    && item_impl.trait_.is_none()
97                {
98                    self.check_visibility_order(
99                        &crate::cmds::rsl::ast::impl_nodes(item_impl),
100                        ctx.path,
101                        &mut violations,
102                    );
103                }
104            }
105        }
106
107        violations
108    }
109}
110
111#[derive(Debug)]
112#[cfg_attr(test, derive(Eq, PartialEq))]
113pub struct MisorderedVisibilityViolation {
114    pub location: Location,
115    pub details: MisorderedVisibilityDetails,
116}
117
118impl MisorderedVisibilityViolation {
119    fn new(path: &Path, span: Span, expected_before: String, item_label: String) -> Self {
120        Self {
121            location: Location::from_span(path, span),
122            details: MisorderedVisibilityDetails {
123                expected_before,
124                item_label,
125            },
126        }
127    }
128}
129
130impl TypedRuleViolation for MisorderedVisibilityViolation {
131    type Rule = MisorderedVisibilityRule;
132}
133
134#[derive(Debug)]
135#[cfg_attr(test, derive(Eq, PartialEq))]
136pub struct MisorderedVisibilityDetails {
137    pub expected_before: String,
138    pub item_label: String,
139}
140
141#[cfg(test)]
142mod tests {
143    use std::path::PathBuf;
144
145    use test_that::prelude::*;
146
147    use super::MisorderedVisibilityDetails;
148    use super::MisorderedVisibilityRule;
149    use super::MisorderedVisibilityViolation;
150    use crate::cmds::rsl::rules::TypedRule;
151    use crate::cmds::rsl::rules::common::Location;
152
153    #[test]
154    fn test_misordered_visibility_rule_check_when_visibility_decreases_reports_public_item() {
155        let syntax = syn::parse_file(
156            r"
157            fn private() {}
158            pub fn public() {}
159            ",
160        )
161        .unwrap();
162
163        let result = MisorderedVisibilityRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
164
165        assert_that!(
166            result,
167            eq(vec![MisorderedVisibilityViolation {
168                location: Location::new(PathBuf::from("test.rs"), 3, 17),
169                details: MisorderedVisibilityDetails {
170                    expected_before: "fn private".to_owned(),
171                    item_label: "fn public".to_owned(),
172                },
173            }])
174        );
175    }
176
177    #[test]
178    fn test_misordered_visibility_rule_check_when_type_is_more_visible_than_previous_item_reports_type() {
179        let syntax = syn::parse_file(
180            r"
181            fn private() {}
182            pub struct Data;
183            impl Data {}
184            ",
185        )
186        .unwrap();
187
188        let result = MisorderedVisibilityRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
189
190        assert_that!(
191            result,
192            eq(vec![MisorderedVisibilityViolation {
193                location: Location::new(PathBuf::from("test.rs"), 3, 17),
194                details: MisorderedVisibilityDetails {
195                    expected_before: "fn private".to_owned(),
196                    item_label: "struct Data".to_owned(),
197                },
198            }])
199        );
200    }
201
202    #[test]
203    fn test_misordered_visibility_rule_check_when_associated_visibility_decreases_reports_public_item() {
204        let syntax = syn::parse_file(
205            r"
206            struct Data;
207            impl Data {
208                fn helper() {}
209                pub fn api() {}
210            }
211            ",
212        )
213        .unwrap();
214
215        let result = MisorderedVisibilityRule::new(None).check(&crate::cmds::rsl::rules::test_ctx(&syntax));
216
217        assert_that!(
218            result,
219            eq(vec![MisorderedVisibilityViolation {
220                location: Location::new(PathBuf::from("test.rs"), 5, 21),
221                details: MisorderedVisibilityDetails {
222                    expected_before: "fn helper".to_owned(),
223                    item_label: "fn api".to_owned(),
224                },
225            }])
226        );
227    }
228}