11use darling:: { util:: SpannedValue , Error , FromMeta } ;
22use k8s_version:: Version ;
33use 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+ }
599
6100/// These attributes are meant to be used in super structs, which add
7101/// [`Field`](syn::Field) or [`Variant`](syn::Variant) specific attributes via
8102/// 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.
9112#[ derive( Debug , FromMeta ) ]
10- #[ darling( and_then = ItemAttributes :: validate) ]
11113pub ( crate ) struct ItemAttributes {
12114 /// This parses the `added` attribute on items (fields or variants). It can
13115 /// only be present at most once.
@@ -24,8 +126,12 @@ pub(crate) struct ItemAttributes {
24126}
25127
26128impl 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 ( ) ;
29135
30136 // TODO (@Techassi): Make the field 'note' optional, because in the
31137 // future, the macro will generate parts of the deprecation note
@@ -34,12 +140,142 @@ impl ItemAttributes {
34140
35141 if let Some ( deprecated) = & self . deprecated {
36142 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+ ) ;
39147 }
40148 }
41149
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 ( ( ) )
43279 }
44280}
45281
0 commit comments