wowlab_tidy/languages/rust/rules/api/
ctor_new.rs1use 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});