Skip to main content

wowlab_tidy/languages/rust/rules/api/
ctor_new.rs

1use ra_ap_syntax::ast::{self, HasName, HasVisibility, VisibilityKind};
2use wowlab_types::sim::FastSet;
3
4use super::support::{has_derive, type_name};
5use crate::{AstCtx, Example, Violation};
6
7#[rustfmt::skip]
8const EXAMPLES: &[Example] = &[
9    Example {
10        label: "derived Default without new",
11        code: "#[derive(Default)]\npub struct Pool { size: u32 }",
12        pass: false,
13    },
14    Example {
15        label: "manual Default without new",
16        code: "pub struct Pool { size: u32 }\nimpl Default for Pool {\n    fn default() -> Self {\n        Pool { size: 0 }\n    }\n}",
17        pass: false,
18    },
19    Example {
20        label: "private new only",
21        code: "#[derive(Default)]\npub struct Pool { size: u32 }\nimpl Pool {\n    fn new() -> Self {\n        Pool { size: 0 }\n    }\n}",
22        pass: false,
23    },
24    Example {
25        label: "Default with pub new",
26        code: "#[derive(Default)]\npub struct Pool { size: u32 }\nimpl Pool {\n    pub fn new() -> Self {\n        Pool { size: 0 }\n    }\n}",
27        pass: true,
28    },
29    Example {
30        label: "no Default",
31        code: "pub struct Pool { size: u32 }",
32        pass: true,
33    },
34    Example {
35        label: "struct-literal constructible",
36        code: "#[derive(Default)]\npub struct Point { pub x: f32, pub y: f32 }",
37        pass: true,
38    },
39    Example {
40        label: "private struct",
41        code: "#[derive(Default)]\nstruct Pool { size: u32 }",
42        pass: true,
43    },
44    Example {
45        label: "Default without new in test module",
46        code: "#[cfg(test)]\nmod tests {\n    #[derive(Default)]\n    pub struct Pool { size: u32 }\n}",
47        pass: true,
48    },
49];
50
51crate::ast_rule!(
52    ctor_new,
53    "Flag public structs with `Default` but no `pub fn new` — constructors are static inherent methods (C-CTOR).",
54    "Users reach for X::new() first; a type offering only Default surprises them and breaks the upstream C-CTOR convention.",
55    Low,
56);
57
58fn check_ctor_new(ctx: &AstCtx<'_>) -> Vec<Violation> {
59    let (default_impls, pub_new) = collect_ctor_info(ctx);
60
61    let public_structs = ctx
62        .nodes::<ast::Struct>()
63        .filter(|item| !ctx.is_in_test(item))
64        .filter(|item| is_pub(item.visibility()))
65        .filter(has_private_field);
66
67    public_structs
68        .filter(|item| {
69            item.name().is_some_and(|name| {
70                has_derive(item, "Default") || default_impls.contains(name.text().as_str())
71            })
72        })
73        .filter_map(|item| {
74            let name = item.name()?;
75
76            (!pub_new.contains(name.text().as_str())).then(|| {
77                ctx.violation(
78                    &name,
79                    format!(
80                        "public struct `{name}` implements Default but has no `pub fn new` (C-CTOR)"
81                    ),
82                )
83            })
84        })
85        .collect()
86}
87
88fn collect_ctor_info(ctx: &AstCtx<'_>) -> (FastSet<String>, FastSet<String>) {
89    let mut default_impls = FastSet::default();
90    let mut pub_new = FastSet::default();
91
92    for item in ctx.nodes::<ast::Impl>() {
93        let Some(ty) = item.self_ty().and_then(|ty| type_name(&ty)) else {
94            continue;
95        };
96
97        match item.trait_().and_then(|ty| type_name(&ty)).as_deref() {
98            Some("Default") => {
99                default_impls.insert(ty);
100            }
101            None if has_pub_new(&item) => {
102                pub_new.insert(ty);
103            }
104            _ => {}
105        }
106    }
107
108    (default_impls, pub_new)
109}
110
111fn has_pub_new(item: &ast::Impl) -> bool {
112    item.assoc_item_list().is_some_and(|list| {
113        list.assoc_items().any(|assoc| match assoc {
114            ast::AssocItem::Fn(function) => {
115                function.name().is_some_and(|name| name.text() == "new")
116                    && is_pub(function.visibility())
117            }
118            _ => false,
119        })
120    })
121}
122
123fn has_private_field(item: &ast::Struct) -> bool {
124    item.field_list().is_some_and(|list| match list {
125        ast::FieldList::RecordFieldList(list) => {
126            list.fields().any(|field| !is_pub(field.visibility()))
127        }
128        ast::FieldList::TupleFieldList(list) => {
129            list.fields().any(|field| !is_pub(field.visibility()))
130        }
131    })
132}
133
134fn is_pub(visibility: Option<ast::Visibility>) -> bool {
135    visibility.is_some_and(|vis| matches!(vis.kind(), VisibilityKind::Pub))
136}
137
138crate::tidy_ast_test!(check_ctor_new, {
139    crate::example_tests!(EXAMPLES, check_ctor_new);
140});