wowlab_tidy/languages/rust/rules/api/
non_exhaustive_on_public.rs1use ra_ap_syntax::ast::{self, HasAttrs, HasName, HasVisibility, VisibilityKind};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7 Example {
8 label: "public enum without non_exhaustive",
9 code: "pub enum Color { Red, Green, Blue }",
10 pass: false,
11 },
12 Example {
13 label: "public enum with non_exhaustive",
14 code: "#[non_exhaustive]\npub enum Color { Red, Green, Blue }",
15 pass: true,
16 },
17 Example {
18 label: "private enum without non_exhaustive",
19 code: "enum Color { Red, Green, Blue }",
20 pass: true,
21 },
22 Example {
23 label: "pub(crate) enum without non_exhaustive",
24 code: "pub(crate) enum Color { Red, Green, Blue }",
25 pass: true,
26 },
27 Example {
28 label: "public enum in test",
29 code: "#[cfg(test)]\nmod tests {\n pub enum Color { Red, Green, Blue }\n}",
30 pass: true,
31 },
32];
33
34crate::ast_rule!(
35 non_exhaustive_on_public,
36 "Flag public enums without `#[non_exhaustive]` — prevents breaking changes when adding variants.",
37 "Adding a variant to a public enum is a breaking change. #[non_exhaustive] lets you add variants without a major version bump.",
38 Medium,
39);
40
41fn check_non_exhaustive_on_public(ctx: &AstCtx<'_>) -> Vec<Violation> {
42 let public_enums = ctx
43 .nodes::<ast::Enum>()
44 .filter(|item| !ctx.is_in_test(item))
45 .filter(|item| {
46 item.visibility()
47 .is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
48 });
49
50 public_enums
51 .filter(|item| {
52 !item
53 .attrs()
54 .any(|attr| attr.simple_name().as_deref() == Some("non_exhaustive"))
55 })
56 .filter_map(|item| {
57 let name = item.name()?;
58
59 Some(ctx.violation(
60 &name,
61 format!(
62 "public enum `{name}` should have `#[non_exhaustive]` to allow adding variants without breaking downstream"
63 ),
64 ))
65 })
66 .collect()
67}
68
69crate::tidy_ast_test!(check_non_exhaustive_on_public, {
70 crate::example_tests!(EXAMPLES, check_non_exhaustive_on_public);
71});