1
1
use darling:: { util:: SpannedValue , Error , FromMeta } ;
2
2
use k8s_version:: Version ;
3
3
use proc_macro2:: Span ;
4
- use syn:: Path ;
4
+ use syn:: { spanned:: Spanned , Ident , Path } ;
5
+
6
+ use crate :: {
7
+ attrs:: common:: ContainerAttributes ,
8
+ codegen:: common:: Attributes ,
9
+ consts:: { DEPRECATED_FIELD_PREFIX , DEPRECATED_VARIANT_PREFIX } ,
10
+ } ;
11
+
12
+ /// This trait helps to unify attribute validation for both field and variant
13
+ /// attributes.
14
+ ///
15
+ /// This trait is implemented using a blanket implementation on types
16
+ /// `T: Attributes`. The [`Attributes`] trait allows access to the common
17
+ /// attributes shared across field and variant attributes.
18
+ pub ( crate ) trait ValidateVersions < I >
19
+ where
20
+ I : Spanned ,
21
+ {
22
+ /// Validates that each field action version is present in the declared
23
+ /// container versions.
24
+ fn validate_versions (
25
+ & self ,
26
+ container_attrs : & ContainerAttributes ,
27
+ item : & I ,
28
+ ) -> Result < ( ) , darling:: Error > ;
29
+ }
30
+
31
+ impl < I , T > ValidateVersions < I > for T
32
+ where
33
+ T : Attributes ,
34
+ I : Spanned ,
35
+ {
36
+ fn validate_versions (
37
+ & self ,
38
+ container_attrs : & ContainerAttributes ,
39
+ item : & I ,
40
+ ) -> Result < ( ) , darling:: Error > {
41
+ // NOTE (@Techassi): Can we maybe optimize this a little?
42
+
43
+ let mut errors = Error :: accumulator ( ) ;
44
+
45
+ if let Some ( added) = & self . common_attrs ( ) . added {
46
+ if !container_attrs
47
+ . versions
48
+ . iter ( )
49
+ . any ( |v| v. name == * added. since )
50
+ {
51
+ errors. push ( Error :: custom (
52
+ "variant action `added` uses version which was not declared via #[versioned(version)]" )
53
+ . with_span ( item)
54
+ ) ;
55
+ }
56
+ }
57
+
58
+ for rename in & * self . common_attrs ( ) . renames {
59
+ if !container_attrs
60
+ . versions
61
+ . iter ( )
62
+ . any ( |v| v. name == * rename. since )
63
+ {
64
+ errors. push (
65
+ Error :: custom ( "variant action `renamed` uses version which was not declared via #[versioned(version)]" )
66
+ . with_span ( item)
67
+ ) ;
68
+ }
69
+ }
70
+
71
+ if let Some ( deprecated) = & self . common_attrs ( ) . deprecated {
72
+ if !container_attrs
73
+ . versions
74
+ . iter ( )
75
+ . any ( |v| v. name == * deprecated. since )
76
+ {
77
+ errors. push ( Error :: custom (
78
+ "variant action `deprecated` uses version which was not declared via #[versioned(version)]" )
79
+ . with_span ( item)
80
+ ) ;
81
+ }
82
+ }
83
+
84
+ errors. finish ( ) ?;
85
+ Ok ( ( ) )
86
+ }
87
+ }
88
+
89
+ // NOTE (@Techassi): It might be possible (but is it required) to move this
90
+ // functionality into a shared trait, which knows what type of item 'Self' is.
91
+
92
+ /// This enum is used to run different validation based on the type of item.
93
+ #[ derive( Debug , strum:: Display ) ]
94
+ #[ strum( serialize_all = "lowercase" ) ]
95
+ pub ( crate ) enum ItemType {
96
+ Field ,
97
+ Variant ,
98
+ }
5
99
6
100
/// These attributes are meant to be used in super structs, which add
7
101
/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via
8
102
/// darling's flatten feature. This struct only provides shared attributes.
103
+ ///
104
+ /// ### Shared Item Rules
105
+ ///
106
+ /// - An item can only ever be added once at most. An item not marked as 'added'
107
+ /// is part of the container in every version until renamed or deprecated.
108
+ /// - An item can be renamed many times. That's why renames are stored in a
109
+ /// [`Vec`].
110
+ /// - An item can only be deprecated once. A field not marked as 'deprecated'
111
+ /// will be included up until the latest version.
9
112
#[ derive( Debug , FromMeta ) ]
10
- #[ darling( and_then = ItemAttributes :: validate) ]
11
113
pub ( crate ) struct ItemAttributes {
12
114
/// This parses the `added` attribute on items (fields or variants). It can
13
115
/// only be present at most once.
@@ -24,8 +126,12 @@ pub(crate) struct ItemAttributes {
24
126
}
25
127
26
128
impl ItemAttributes {
27
- fn validate ( self ) -> Result < Self , Error > {
28
- // Validate deprecated options
129
+ pub ( crate ) fn validate ( & self , item_ident : & Ident , item_type : & ItemType ) -> Result < ( ) , Error > {
130
+ // NOTE (@Techassi): This associated function is NOT called by darling's
131
+ // and_then attribute, but instead by the wrapper, FieldAttributes and
132
+ // VariantAttributes.
133
+
134
+ let mut errors = Error :: accumulator ( ) ;
29
135
30
136
// TODO (@Techassi): Make the field 'note' optional, because in the
31
137
// future, the macro will generate parts of the deprecation note
@@ -34,12 +140,142 @@ impl ItemAttributes {
34
140
35
141
if let Some ( deprecated) = & self . deprecated {
36
142
if deprecated. note . is_empty ( ) {
37
- return Err ( Error :: custom ( "deprecation note must not be empty" )
38
- . with_span ( & deprecated. note . span ( ) ) ) ;
143
+ errors. push (
144
+ Error :: custom ( "deprecation note must not be empty" )
145
+ . with_span ( & deprecated. note . span ( ) ) ,
146
+ ) ;
39
147
}
40
148
}
41
149
42
- Ok ( self )
150
+ // Semantic validation
151
+ errors. handle ( self . validate_action_combinations ( item_ident, item_type) ) ;
152
+ errors. handle ( self . validate_action_order ( item_ident, item_type) ) ;
153
+ errors. handle ( self . validate_field_name ( item_ident, item_type) ) ;
154
+
155
+ // TODO (@Techassi): Add hint if a field is added in the first version
156
+ // that it might be clever to remove the 'added' attribute.
157
+
158
+ errors. finish ( ) ?;
159
+
160
+ Ok ( ( ) )
161
+ }
162
+
163
+ /// This associated function is called by the top-level validation function
164
+ /// and validates that each item uses a valid combination of actions.
165
+ /// Invalid combinations are:
166
+ ///
167
+ /// - `added` and `deprecated` using the same version: A field cannot be
168
+ /// marked as added in a particular version and then marked as deprecated
169
+ /// immediately after. Fields must be included for at least one version
170
+ /// before being marked deprecated.
171
+ /// - `added` and `renamed` using the same version: The same reasoning from
172
+ /// above applies here as well. Fields must be included for at least one
173
+ /// version before being renamed.
174
+ /// - `renamed` and `deprecated` using the same version: Again, the same
175
+ /// rules from above apply here as well.
176
+ fn validate_action_combinations (
177
+ & self ,
178
+ item_ident : & Ident ,
179
+ item_type : & ItemType ,
180
+ ) -> Result < ( ) , Error > {
181
+ match ( & self . added , & self . renames , & self . deprecated ) {
182
+ ( Some ( added) , _, Some ( deprecated) ) if * added. since == * deprecated. since => {
183
+ Err ( Error :: custom ( format ! (
184
+ "{item_type} cannot be marked as `added` and `deprecated` in the same version"
185
+ ) )
186
+ . with_span ( item_ident) )
187
+ }
188
+ ( Some ( added) , renamed, _) if renamed. iter ( ) . any ( |r| * r. since == * added. since ) => {
189
+ Err ( Error :: custom ( format ! (
190
+ "{item_type} cannot be marked as `added` and `renamed` in the same version"
191
+ ) )
192
+ . with_span ( item_ident) )
193
+ }
194
+ ( _, renamed, Some ( deprecated) )
195
+ if renamed. iter ( ) . any ( |r| * r. since == * deprecated. since ) =>
196
+ {
197
+ Err ( Error :: custom (
198
+ "field cannot be marked as `deprecated` and `renamed` in the same version" ,
199
+ )
200
+ . with_span ( item_ident) )
201
+ }
202
+ _ => Ok ( ( ) ) ,
203
+ }
204
+ }
205
+
206
+ /// This associated function is called by the top-level validation function
207
+ /// and validates that actions use a chronologically sound chain of
208
+ /// versions.
209
+ ///
210
+ /// The following rules apply:
211
+ ///
212
+ /// - `deprecated` must use a greater version than `added`: This function
213
+ /// ensures that these versions are chronologically sound, that means,
214
+ /// that the version of the deprecated action must be greater than the
215
+ /// version of the added action.
216
+ /// - All `renamed` actions must use a greater version than `added` but a
217
+ /// lesser version than `deprecated`.
218
+ fn validate_action_order ( & self , item_ident : & Ident , item_type : & ItemType ) -> Result < ( ) , Error > {
219
+ let added_version = self . added . as_ref ( ) . map ( |a| * a. since ) ;
220
+ let deprecated_version = self . deprecated . as_ref ( ) . map ( |d| * d. since ) ;
221
+
222
+ // First, validate that the added version is less than the deprecated
223
+ // version.
224
+ // NOTE (@Techassi): Is this already covered by the code below?
225
+ if let ( Some ( added_version) , Some ( deprecated_version) ) = ( added_version, deprecated_version)
226
+ {
227
+ if added_version > deprecated_version {
228
+ return Err ( Error :: custom ( format ! (
229
+ "{item_type} was marked as `added` in version `{added_version}` while being marked as `deprecated` in an earlier version `{deprecated_version}`"
230
+ ) ) . with_span ( item_ident) ) ;
231
+ }
232
+ }
233
+
234
+ // Now, iterate over all renames and ensure that their versions are
235
+ // between the added and deprecated version.
236
+ if !self . renames . iter ( ) . all ( |r| {
237
+ added_version. map_or ( true , |a| a < * r. since )
238
+ && deprecated_version. map_or ( true , |d| d > * r. since )
239
+ } ) {
240
+ return Err ( Error :: custom (
241
+ "all renames must use versions higher than `added` and lower than `deprecated`" ,
242
+ )
243
+ . with_span ( item_ident) ) ;
244
+ }
245
+
246
+ Ok ( ( ) )
247
+ }
248
+
249
+ /// This associated function is called by the top-level validation function
250
+ /// and validates that items use correct names depending on attached
251
+ /// actions.
252
+ ///
253
+ /// The following naming rules apply:
254
+ ///
255
+ /// - Fields marked as deprecated need to include the 'deprecated_' prefix
256
+ /// in their name. The prefix must not be included for fields which are
257
+ /// not deprecated.
258
+ fn validate_field_name ( & self , item_ident : & Ident , item_type : & ItemType ) -> Result < ( ) , Error > {
259
+ let prefix = match item_type {
260
+ ItemType :: Field => DEPRECATED_FIELD_PREFIX ,
261
+ ItemType :: Variant => DEPRECATED_VARIANT_PREFIX ,
262
+ } ;
263
+
264
+ let starts_with_deprecated = item_ident. to_string ( ) . starts_with ( prefix) ;
265
+
266
+ if self . deprecated . is_some ( ) && !starts_with_deprecated {
267
+ return Err ( Error :: custom (
268
+ format ! ( "{item_type} was marked as `deprecated` and thus must include the `{prefix}` prefix in its name" )
269
+ ) . with_span ( item_ident) ) ;
270
+ }
271
+
272
+ if self . deprecated . is_none ( ) && starts_with_deprecated {
273
+ return Err ( Error :: custom (
274
+ format ! ( "{item_type} includes the `{prefix}` prefix in its name but is not marked as `deprecated`" )
275
+ ) . with_span ( item_ident) ) ;
276
+ }
277
+
278
+ Ok ( ( ) )
43
279
}
44
280
}
45
281
0 commit comments