1#![warn(
4 missing_debug_implementations,
5 missing_docs,
6 rust_2018_idioms,
7 unreachable_pub,
8 clippy::cargo,
9 clippy::must_use_candidate
10)]
11
12use proc_macro::{self, Span, TokenStream};
13use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
14use quote::{quote, quote_spanned, ToTokens};
15use syn::{
16 parse_macro_input, Attribute, Error, Field, Fields, GenericParam, Ident, ItemStruct, Type,
17};
18
19const NO_EQ: &str = "no_eq";
20const DO_NOT_TRACK: &str = "do_not_track";
21
22#[proc_macro_attribute]
24pub fn track(attr: TokenStream, item: TokenStream) -> TokenStream {
25 if !attr.is_empty() {
26 return Error::new(
27 attr.into_iter().next().unwrap().span().into(),
28 "This macro doesn't handle attributes",
29 )
30 .into_compile_error()
31 .into();
32 }
33
34 let mut data: ItemStruct = parse_macro_input!(item);
35 let ident = data.ident.clone();
36 let tracker_ty;
37 let struct_vis = &data.vis;
38 let where_clause = &data.generics.where_clause;
39
40 let mut generics = data.generics.clone();
42 for param in generics.params.iter_mut() {
43 if let GenericParam::Type(ty) = param {
44 ty.eq_token = None;
45 ty.default = None;
46 }
47 }
48
49 let mut generics_iter = data.generics.params.iter();
50 let mut generic_idents = TokenStream2::new();
51
52 if let Some(first) = generics_iter.next() {
53 impl_struct_generics(first, &mut generic_idents);
54 for generic_param in generics_iter {
55 generic_idents.extend(quote! {,});
56 impl_struct_generics(generic_param, &mut generic_idents);
57 }
58 }
59
60 let mut field_list = Vec::new();
61 if let Fields::Named(named_fields) = &mut data.fields {
62 for field in &mut named_fields.named {
63 let (do_not_track, no_eq) = parse_field_attrs(&mut field.attrs);
64 if !do_not_track {
65 let ident = field.ident.clone().expect("Field has no identifier");
66 let ty: Type = field.ty.clone();
67 field_list.push((ident, ty, no_eq, field.vis.clone()));
68 }
69 }
70
71 tracker_ty = tracker_type(field_list.len());
72 let change_field = Field {
73 attrs: Vec::new(),
74 vis: syn::Visibility::Inherited,
75 mutability: syn::FieldMutability::None,
76 ident: Some(Ident::new("tracker", Span::call_site().into())),
77 colon_token: None,
78 ty: Type::Verbatim(tracker_ty.clone()),
79 };
80
81 named_fields.named.push(change_field);
82 } else {
83 panic!("No named fields");
84 }
85
86 let mut output = data.to_token_stream();
87
88 let mut methods = proc_macro2::TokenStream::new();
89 for (num, (id, ty, no_eq, vis)) in field_list.iter().enumerate() {
90 let id_span: Span2 = id.span().unwrap().into();
91
92 let get_id = Ident::new(&format!("get_{}", id), id_span);
93 let get_mut_id = Ident::new(&format!("get_mut_{}", id), id_span);
94 let update_id = Ident::new(&format!("update_{}", id), id_span);
95 let changed_id = Ident::new(&format!("changed_{}", id), id_span);
96 let set_id = Ident::new(&format!("set_{}", id), id_span);
97
98 let get_doc = format!("Get an immutable reference to the {id} field.");
99 let get_mut_doc =
100 format!("Get a mutable reference to the {id} field and mark the field as changed.");
101 let update_doc =
102 format!("Use a closure to update the {id} field and mark the field as changed.");
103 let changed_doc = format!("Check if value of {id} field has changed.");
104 let bit_mask_doc = format!("Get a bit mask to look for changes on the {id} field.");
105
106 methods.extend(quote_spanned! { id_span =>
107 #[allow(dead_code, non_snake_case)]
108 #[must_use]
109 #[doc = #get_doc]
110 #vis fn #get_id(&self) -> &#ty {
111 &self.#id
112 }
113
114 #[allow(dead_code, non_snake_case)]
115 #[must_use]
116 #[doc = #get_mut_doc]
117 #vis fn #get_mut_id(&mut self) -> &mut #ty {
118 self.tracker |= Self::#id();
119 &mut self.#id
120 }
121
122 #[allow(dead_code, non_snake_case)]
123 #[doc = #update_doc]
124 #vis fn #update_id<F: FnOnce(&mut #ty)>(&mut self, f: F) {
125 self.tracker |= Self::#id();
126 f(&mut self.#id);
127 }
128
129 #[allow(dead_code, non_snake_case)]
130 #[doc = #changed_doc]
131 #vis fn #changed_id(&self) -> bool {
132 self.changed(Self::#id())
133 }
134
135 #[allow(dead_code, non_snake_case)]
136 #[must_use]
137 #[doc = #bit_mask_doc]
138 #vis fn #id() -> #tracker_ty {
139 1 << #num
140 }
141 });
142
143 if *no_eq {
144 let set_doc = format!("Set the value of field {id} and mark the field as changed.");
145 methods.extend(quote_spanned! { id_span =>
146 #[allow(dead_code, non_snake_case)]
147 #[doc = #set_doc]
148 #vis fn #set_id(&mut self, value: #ty) {
149 self.tracker |= Self::#id();
150 self.#id = value;
151 }
152 });
153 } else {
154 let set_doc = format!("Set the value of field {id} and mark the field as changed if it's not equal to the previous value.");
155 methods.extend(quote_spanned! { id_span =>
156 #[allow(dead_code, non_snake_case)]
157 #[doc = #set_doc]
158 #vis fn #set_id(&mut self, value: #ty) {
159 if self.#id != value {
160 self.tracker |= Self::#id();
161 }
162 self.#id = value;
163 }
164 });
165 }
166 }
167
168 output.extend(quote_spanned! { ident.span() =>
169 impl #generics #ident < #generic_idents > #where_clause {
170 #methods
171 #[allow(dead_code)]
172 #[must_use]
173 #struct_vis fn track_all() -> #tracker_ty {
175 #tracker_ty::MAX
176 }
177
178 #[allow(dead_code)]
179 #struct_vis fn mark_all_changed(&mut self) {
181 self.tracker = #tracker_ty::MAX;
182 }
183
184 #[warn(dead_code)]
189 #[must_use]
190 #struct_vis fn changed(&self, mask: #tracker_ty) -> bool {
191 self.tracker & mask != 0
192 }
193
194 #[allow(dead_code)]
196 #[must_use]
197 #struct_vis fn changed_any(&self) -> bool {
198 self.tracker != 0
199 }
200
201 #[warn(dead_code)]
204 #struct_vis fn reset(&mut self) {
205 self.tracker = 0;
206 }
207 }
208 });
209
210 output.into()
211}
212
213fn impl_struct_generics(param: &GenericParam, stream: &mut TokenStream2) {
214 match param {
215 GenericParam::Type(ty) => ty.ident.to_tokens(stream),
216 GenericParam::Const(cnst) => cnst.to_tokens(stream),
217 GenericParam::Lifetime(lifetime) => lifetime.to_tokens(stream),
218 }
219}
220
221fn parse_field_attrs(attrs: &mut Vec<Attribute>) -> (bool, bool) {
224 let mut do_not_track = false;
225 let mut no_eq = false;
226 let attrs_clone = attrs.clone();
227
228 for (index, attr) in attrs_clone.iter().enumerate() {
229 let segs = &attr.path().segments;
230 match segs.len() {
231 1 => {
232 let first = &segs.first().unwrap().ident;
233 if first == NO_EQ {
234 attrs.remove(index);
235 no_eq = true;
236 } else if first == DO_NOT_TRACK {
237 attrs.remove(index);
238 do_not_track = true;
239 }
240 }
241 2 => {
242 let mut iter = segs.iter();
243 let first = &iter.next().unwrap().ident;
244 if first == "tracker" {
245 let second = &iter.next().unwrap().ident;
246 if second == NO_EQ {
247 attrs.remove(index);
248 no_eq = true;
249 } else if second == DO_NOT_TRACK {
250 attrs.remove(index);
251 do_not_track = true;
252 }
253 }
254 }
255 _ => {}
256 }
257 }
258
259 (do_not_track, no_eq)
260}
261
262fn tracker_type(len: usize) -> proc_macro2::TokenStream {
263 match len {
264 0..=8 => {
265 quote! {u8}
266 }
267 9..=16 => {
268 quote! {u16}
269 }
270 17..=32 => {
271 quote! {u32}
272 }
273 33..=64 => {
274 quote! {u64}
275 }
276 65..=128 => {
277 quote! {u128}
278 }
279 _ => {
280 panic!("You can only track up to 128 values")
281 }
282 }
283}