wowlab_tidy/languages/rust/rules/performance/
vec_string_field.rs1use ra_ap_syntax::{
2 AstNode, ast,
3 ast::{HasGenericArgs, HasVisibility},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "private Vec<String> field",
12 code: "struct S { names: Vec<String> }",
13 pass: false,
14 },
15 Example {
16 label: "private Vec<Vec<T>> field",
17 code: "struct S { grid: Vec<Vec<u8>> }",
18 pass: false,
19 },
20 Example {
21 label: "pub(crate) field is not fully public",
22 code: "pub struct S { pub(crate) names: Vec<String> }",
23 pass: false,
24 },
25 Example {
26 label: "private tuple-struct field",
27 code: "struct S(Vec<String>);",
28 pass: false,
29 },
30 Example {
31 label: "fully public field is user-visible",
32 code: "pub struct S { pub names: Vec<String> }",
33 pass: true,
34 },
35 Example {
36 label: "already boxed str elements",
37 code: "struct S { names: Vec<Box<str>> }",
38 pass: true,
39 },
40 Example {
41 label: "primitive element vector",
42 code: "struct S { ids: Vec<u32> }",
43 pass: true,
44 },
45 Example {
46 label: "Vec<String> field in test module",
47 code: "#[cfg(test)]\nmod tests {\n struct S { names: Vec<String> }\n}",
48 pass: true,
49 },
50 Example {
51 label: "record enum variant is outside the struct-only rule",
52 code: "enum E { Names { values: Vec<String> } }",
53 pass: true,
54 },
55 Example {
56 label: "tuple enum variant is outside the struct-only rule",
57 code: "enum E { Names(Vec<String>) }",
58 pass: true,
59 },
60];
61
62crate::ast_rule!(
63 vec_string_field,
64 "Flag non-pub struct fields typed `Vec<String>` or `Vec<Vec<T>>`.",
65 "Immutable-after-construction sequences stored as Vec<Box<str>>/Vec<Box<[T]>> drop the capacity word and imply shrink-to-fit.",
66);
67
68fn check_vec_string_field(ctx: &AstCtx<'_>) -> Vec<Violation> {
69 let records = ctx
70 .nodes::<ast::RecordField>()
71 .filter(|field| !ctx.is_in_test(field) && is_struct_field(field))
72 .filter_map(|field| field_message(field.visibility(), field.ty()).map(|msg| (field, msg)));
73 let tuples = ctx
74 .nodes::<ast::TupleField>()
75 .filter(|field| !ctx.is_in_test(field) && is_struct_field(field))
76 .filter_map(|field| field_message(field.visibility(), field.ty()).map(|msg| (field, msg)));
77
78 records
79 .map(|(field, message)| ctx.violation(&field, message))
80 .chain(tuples.map(|(field, message)| ctx.violation(&field, message)))
81 .collect()
82}
83
84fn is_struct_field<N>(field: &N) -> bool
85where
86 N: AstNode,
87{
88 field
89 .syntax()
90 .ancestors()
91 .skip(1)
92 .find_map(|ancestor| {
93 if ast::Struct::can_cast(ancestor.kind()) {
94 Some(true)
95 } else if ast::Variant::can_cast(ancestor.kind()) {
96 Some(false)
97 } else {
98 None
99 }
100 })
101 .unwrap_or(false)
102}
103
104const MSG_STRING: &str = "non-pub `Vec<String>` field — if immutable after construction, prefer `Vec<Box<str>>` (drops the capacity word, implies shrink-to-fit)";
105const MSG_VEC: &str = "non-pub `Vec<Vec<T>>` field — if immutable after construction, prefer `Vec<Box<[T]>>` (drops the capacity word, implies shrink-to-fit)";
106
107fn vec_inner_ident(ty: &ast::Type) -> Option<String> {
108 let ast::Type::PathType(path_type) = ty else {
109 return None;
110 };
111 let segment = path_type.path()?.segment()?;
112
113 if segment.name_ref()?.text() != "Vec" {
114 return None;
115 }
116
117 segment.generic_arg_list()?.generic_args().find_map(|arg| {
118 let ast::GenericArg::TypeArg(arg) = arg else {
119 return None;
120 };
121 let ast::Type::PathType(inner) = arg.ty()? else {
122 return None;
123 };
124
125 inner
126 .path()?
127 .segment()?
128 .name_ref()
129 .map(|name| name.text().to_string())
130 })
131}
132
133fn field_message(
134 visibility: Option<ast::Visibility>,
135 ty: Option<ast::Type>,
136) -> Option<&'static str> {
137 if visibility.is_some_and(|visibility| visibility.syntax().text() == "pub") {
138 return None;
139 }
140
141 match vec_inner_ident(&ty?).as_deref() {
142 Some("String") => Some(MSG_STRING),
143 Some("Vec") => Some(MSG_VEC),
144 _ => None,
145 }
146}
147
148crate::tidy_ast_test!(check_vec_string_field, {
149 crate::example_tests!(EXAMPLES, check_vec_string_field);
150});