So far, we have built a powerful aspect that implements the Deep Clone pattern and has three pieces of API:
the [Cloneable] and [Child] attributes and the method void CloneMembers(T). Our aspect already reports errors in
unsupported cases. We will now see how we can improve the productivity of the aspect's users by providing coding
guidance.
First, we would like to save users from the need to remember the name and signature of the void CloneMembers(T)
method. When there is no such method in their code, we would like to add an action to the refactoring menu that would
create this action like this:

Secondly, suppose that we have deployed the Cloneable aspect to the team, and we notice that developers frequently
forget to annotate cloneable fields with the [Child] attribute, causing inconsistencies in the resulting cloned object
tree. Such inconsistencies are tedious to debug because they may appear randomly after the cloning process, losing much
time for the team and degrading trust in aspect-oriented programming and architecture decisions. As the aspect's
authors, it is our job to prevent the most frequent pitfalls by reporting a warning and suggesting remediations.
To make sure that developers do not forget to annotate properties with the [Child] attribute, we will define a new
attribute [Reference] and require developers to annotate any cloneable property with either [Child]
or [Reference]. Otherwise, we will report a warning and suggest two code fixes: add [Child] or add [Reference] to
the field. Thanks to this strategy, we ensure that developers no longer forget to classify properties and instead make a
conscious choice.
The first thing developers will experience is the warning:

Note the link Show potential fixes. If developers click on that link or hit Alt+Enter or Ctrl+., they will see two
code suggestions:

Let's see how we can add these features to our aspect.
Aspect implementation
Here is the complete and updated aspect:
1using JetBrains.Annotations;
2using Metalama.Extensions.CodeFixes;
3using Metalama.Framework.Aspects;
4using Metalama.Framework.Code;
5using Metalama.Framework.Diagnostics;
6using Metalama.Framework.Project;
7
8[Inheritable]
9[EditorExperience( SuggestAsLiveTemplate = true )]
10public class CloneableAttribute : TypeAspect
11{
12 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty)>
13 _fieldOrPropertyCannotBeReadOnly =
14 new(
15 "CLONE01",
16 Severity.Error,
17 "The {0} '{1}' cannot be read-only because it is marked as a [Child]." );
18
19 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty, IType)>
20 _missingCloneMethod =
21 new(
22 "CLONE02",
23 Severity.Error,
24 "The {0} '{1}' cannot be a [Child] because its type '{2}' does not have a 'Clone' parameterless method." );
25
26 private static readonly DiagnosticDefinition<IMethod> _cloneMethodMustBePublic =
27 new(
28 "CLONE03",
29 Severity.Error,
30 "The '{0}' method must be public or internal." );
31
32 private static readonly DiagnosticDefinition<IProperty> _childPropertyMustBeAutomatic =
33 new(
34 "CLONE04",
35 Severity.Error,
36 "The property '{0}' cannot be a [Child] because is not an automatic property." );
37
38 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty)>
39 _annotateFieldOrProperty =
40 new( "CLONE05", Severity.Warning, "Mark the {0} '{1}' as a [Child] or [Reference]." );
41
42 public override void BuildAspect( IAspectBuilder<INamedType> builder )
43 {
44 // Verify child fields and properties.
45 if ( !this.VerifyFieldsAndProperties( builder ) )
46 {
47 builder.SkipAspect();
48
49 return;
50 }
51
52 // Introduce the Clone method.
53 builder.IntroduceMethod(
54 nameof(this.CloneImpl),
55 whenExists: OverrideStrategy.Override,
56 args: new { T = builder.Target },
57 buildMethod: m =>
58 {
59 m.Name = "Clone";
60 m.ReturnType = builder.Target;
61 } );
62
63 //
64 builder.IntroduceMethod(
65 nameof(this.CloneMembers),
66 whenExists: OverrideStrategy.Override,
67 args: new { T = builder.Target } );
68
69 //
70
71 // Implement the ICloneable interface.
72 builder.ImplementInterface(
73 typeof(ICloneable),
74 OverrideStrategy.Ignore );
75
76 // When we have non-child fields or properties of a cloneable type,
77 // suggest to add the child attribute
78 var eligibleChildren = builder.Target.FieldsAndProperties
79 .Where( f => f.Writeability == Writeability.All &&
80 !f.IsImplicitlyDeclared &&
81 !f.Attributes.OfAttributeType( typeof(ChildAttribute) ).Any() &&
82 !f.Attributes.OfAttributeType( typeof(ReferenceAttribute) ).Any() &&
83 f.Type is INamedType fieldType &&
84 (fieldType.AllMethods.OfName( "Clone" )
85 .Any( m => m.Parameters.Count == 0 ) ||
86 fieldType.Attributes.OfAttributeType( typeof(CloneableAttribute) )
87 .Any()) );
88
89 //
90 foreach ( var fieldOrProperty in eligibleChildren )
91 {
92 builder.Diagnostics.Report(
93 _annotateFieldOrProperty
94 .WithArguments( (fieldOrProperty.DeclarationKind, fieldOrProperty) )
95 .WithCodeFixes(
96 CodeFixFactory.AddAttribute(
97 fieldOrProperty,
98 typeof(ChildAttribute),
99 "Cloneable | Mark as child" ),
100 CodeFixFactory.AddAttribute(
101 fieldOrProperty,
102 typeof(ReferenceAttribute),
103 "Cloneable | Mark as reference" ) ),
104 fieldOrProperty );
105 }
106
107 //
108
109 //
110 // If we don't have a CloneMember method, suggest to add it.
111 if ( !builder.Target.Methods.OfName( nameof(this.CloneMembers) ).Any() )
112 {
113 builder.Diagnostics.Suggest(
114 new CodeFix(
115 "Cloneable | Customize manually",
116 codeFix =>
117 codeFix.ApplyAspectAsync(
118 builder.Target,
119 new AddEmptyCloneMembersAspect() ) ) );
120 }
121
122 //
123 }
124
125 private bool VerifyFieldsAndProperties( IAspectBuilder<INamedType> builder )
126 {
127 var success = true;
128
129 // Verify that child fields are valid.
130 foreach ( var fieldOrProperty in GetCloneableFieldsOrProperties( builder.Target ) )
131 {
132 // The field or property must be writable.
133 if ( fieldOrProperty.Writeability != Writeability.All )
134 {
135 builder.Diagnostics.Report(
136 _fieldOrPropertyCannotBeReadOnly.WithArguments(
137 (
138 fieldOrProperty.DeclarationKind,
139 fieldOrProperty) ),
140 fieldOrProperty );
141
142 success = false;
143 }
144
145 // If it is a field, it must be an automatic property.
146 if ( fieldOrProperty is IProperty { IsAutoPropertyOrField: false } property )
147 {
148 builder.Diagnostics.Report(
149 _childPropertyMustBeAutomatic.WithArguments( property ),
150 property );
151
152 success = false;
153 }
154
155 // The type of the field must be cloneable.
156 void ReportMissingMethod()
157 {
158 builder.Diagnostics.Report(
159 _missingCloneMethod.WithArguments(
160 (fieldOrProperty.DeclarationKind,
161 fieldOrProperty,
162 fieldOrProperty.Type) ),
163 fieldOrProperty );
164 }
165
166 if ( fieldOrProperty.Type is not INamedType fieldType )
167 {
168 // The field type is an array, a pointer or another special type, which do not have a Clone method.
169 ReportMissingMethod();
170 success = false;
171 }
172 else
173 {
174 var cloneMethod = fieldType.AllMethods.OfName( "Clone" )
175 .SingleOrDefault( p => p.Parameters.Count == 0 );
176
177 if ( cloneMethod == null )
178 {
179 // There is no Clone method.
180 // It may be implemented by an aspect, but we don't have access to aspects on other types
181 // at design time.
182 if ( !MetalamaExecutionContext.Current.ExecutionScenario.IsDesignTime )
183 {
184 if ( !fieldType.BelongsToCurrentProject ||
185 !fieldType.Enhancements().HasAspect<CloneableAttribute>() )
186 {
187 ReportMissingMethod();
188 success = false;
189 }
190 }
191 }
192 else if ( cloneMethod.Accessibility is not (Accessibility.Public
193 or Accessibility.Internal) )
194 {
195 // If we have a Clone method, it must be public.
196 builder.Diagnostics.Report(
197 _cloneMethodMustBePublic.WithArguments( cloneMethod ),
198 fieldOrProperty );
199
200 success = false;
201 }
202 }
203 }
204
205 return success;
206 }
207
208 private static IEnumerable<IFieldOrProperty> GetCloneableFieldsOrProperties( INamedType type )
209 => type.FieldsAndProperties.Where( f =>
210 f.Attributes.OfAttributeType( typeof(ChildAttribute) ).Any() );
211
212 [Template]
213 public virtual T CloneImpl<[CompileTime] T>()
214 {
215 // This compile-time variable will receive the expression representing the base call.
216 // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
217 // we will call MemberwiseClone (this is the initialization of the pattern).
218 IExpression baseCall;
219
220 if ( meta.Target.Method.IsOverride )
221 {
222 baseCall = (IExpression) meta.Base.Clone();
223 }
224 else
225 {
226 baseCall = (IExpression) meta.This.MemberwiseClone();
227 }
228
229 // Define a local variable of the same type as the target type.
230 var clone = (T) baseCall.Value!;
231
232 // Call CloneMembers, which may have a handwritten part.
233 meta.This.CloneMembers( clone );
234
235 return clone;
236 }
237
238 [Template]
239 private void CloneMembers<[CompileTime] T>( T clone )
240 {
241 // Select cloneable fields.
242 var cloneableFields = GetCloneableFieldsOrProperties( meta.Target.Type );
243
244 foreach ( var field in cloneableFields )
245 {
246 // Check if we have a public method 'Clone()' for the type of the field.
247 var fieldType = (INamedType) field.Type;
248
249 field.WithObject( clone ).Value = meta.Cast( fieldType, field.Value?.Clone() );
250 }
251
252 // Call the handwritten implementation, if any.
253 meta.Proceed();
254 }
255
256 [InterfaceMember( IsExplicit = true )]
257 [UsedImplicitly]
258 private object Clone() => meta.This.Clone();
259}
We will first explain the implementation of the second requirement.
Adding warnings with two code fixes
As usual, we first need to define the error as a static field of the class:
38private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty)>
39 _annotateFieldOrProperty =
40 new( "CLONE05", Severity.Warning, "Mark the {0} '{1}' as a [Child] or [Reference]." );
41Then, we detect unannotated properties of a cloneable type. And report the warnings with suggestions for code fixes:
90foreach ( var fieldOrProperty in eligibleChildren )
91{
92 builder.Diagnostics.Report(
93 _annotateFieldOrProperty
94 .WithArguments( (fieldOrProperty.DeclarationKind, fieldOrProperty) )
95 .WithCodeFixes(
96 CodeFixFactory.AddAttribute(
97 fieldOrProperty,
98 typeof(ChildAttribute),
99 "Cloneable | Mark as child" ),
100 CodeFixFactory.AddAttribute(
101 fieldOrProperty,
102 typeof(ReferenceAttribute),
103 "Cloneable | Mark as reference" ) ),
104 fieldOrProperty );
105}
106Notice that we used the WithCodeFixes method to attach code fixes to the diagnostics. To create the code fixes, we use the CodeFixFactory.AddAttribute method. The CodeFixFactory class contains other methods to create simple code fixes.
Suggesting CloneMembers
When we detect that a cloneable type does not already have a CloneMembers method, we suggest adding it without
reporting a warning using the Suggest method:
110// If we don't have a CloneMember method, suggest to add it.
111if ( !builder.Target.Methods.OfName( nameof(this.CloneMembers) ).Any() )
112{
113 builder.Diagnostics.Suggest(
114 new CodeFix(
115 "Cloneable | Customize manually",
116 codeFix =>
117 codeFix.ApplyAspectAsync(
118 builder.Target,
119 new AddEmptyCloneMembersAspect() ) ) );
120}
121Unlike adding attributes, there is no ready-made code fix from the CodeFixFactory class to implement this method. We must implement the code transformation ourselves and provide an instance of the CodeFix class. This object comprises just two elements: the title of the code fix and a delegate performing the code transformation thanks to an ICodeActionBuilder. The list of transformations that are directly available from the ICodeActionBuilder is limited, but we can get enormous power using the ApplyAspectAsync method, which can apply any aspect to any declaration.
To implement the code fix, we create the ad-hoc aspect class AddEmptyCloneMembersAspect, whose implementation should
now be familiar:
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3
4internal class AddEmptyCloneMembersAspect : IAspect<INamedType>
5{
6 public void BuildAspect( IAspectBuilder<INamedType> builder )
7 => builder.IntroduceMethod(
8 nameof(this.CloneMembers),
9 whenExists: OverrideStrategy.Override,
10 args: new { T = builder.Target } );
11
12 // ReSharper disable once UnusedParameter.Local
13
14 [Template]
15 private void CloneMembers<[CompileTime] T>( T clone )
16 {
17 meta.InsertComment( "Use this method to modify the 'clone' parameter." );
18 meta.InsertComment( "Your code executes after the aspect." );
19 }
20}
Note that we did not derive AddEmptyCloneMembersAspect from TypeAspect because it
would make the aspect a custom attribute. Instead, we directly implemented the IAspect
interface.
Summary
We implemented coding guidance into our Cloneable aspect so that our users do not have to look at the design
documentation so often and to prevent them from making frequent mistakes. We used two new techniques: attaching code
fixes to warnings using
the IDiagnostic.WithCodeFixes method and
suggesting code fixes without warning using the Suggest
method.