Skip to main content

wowlab_tidy/languages/rust/rules/performance/
vec_string_field.rs

1use 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});