Clone example, step 1: verifying code
In the previous article, we built an aspect that implements the Deep Clone pattern. The aspect assumes that all fields
or properties annotated with the [Child]
attribute are both of a cloneable type and assignable. If this is not the
case, the aspect generates uncompilable code, making the aspect's user confused.
In this article, we will improve the aspect so that it reports errors in the following three unsupported situations.
We report an error when the field or property is read-only.
1namespace ErrorReadOnlyField;
2
3[Cloneable]
4internal class Game
5{
6 public Player Player { get; }
7
Error CLONE01: The property 'Game.Settings' cannot be read-only because it is marked as a [Child].
8 [Child] public GameSettings Settings { get; }
9}
10
11[Cloneable]
12internal class GameSettings
13{
14 public int Level { get; set; }
15 public string World { get; set; }
16}
17
18internal class Player
19{
20 public string Name { get; }
21}
We report an error when the type of the field or property does not have a Clone
method.
1namespace ErrorNotCloneableChild;
2
3[Cloneable]
4internal class Game
5{
Error CLONE01: The property 'Game.Player' cannot be read-only because it is marked as a [Child].
6 [Child] public Player Player { get; }
7
8 [Child] public GameSettings Settings { get; private set; }
9}
10
11[Cloneable]
12internal class GameSettings
13{
14 public int Level { get; set; }
15 public string World { get; set; }
16}
17
18internal class Player
19{
20 public string Name { get; }
21}
The Clone
method must be public
or internal
.
1namespace ErrorProtectedCloneMethod;
2
3[Cloneable]
4internal class Game
5{
Error CLONE03: The 'Player.Clone()' method must be public or internal.
6 [Child] public Player Player { get; private set; }
7}
8
9internal class Player
10{
11 public string Name { get; init; }
12
13 protected Player Clone() => new() { Name = this.Name };
14}
We report an error when the property is not an automatic property.
1namespace ErrorNotAutomaticProperty;
2
3[Cloneable]
4internal class Game
5{
6 private GameSettings _settings;
7
8 public Player Player { get; }
9
10 [Child]
Error CLONE04: The property 'Game.Settings' cannot be a [Child] because is not an automatic property.
11 public GameSettings Settings
12 {
13 get => this._settings;
14
15 private set
16 {
17 Console.WriteLine( "Setting the value." );
18 this._settings = value;
19 }
20 }
21}
22
23[Cloneable]
24internal class GameSettings
25{
26 public int Level { get; set; }
27 public string World { get; set; }
28}
29
30internal class Player
31{
32 public string Name { get; }
33}
Aspect implementation
The full updated aspect code is here:
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using Metalama.Framework.Project;
5
6[Inheritable]
7[EditorExperience( SuggestAsLiveTemplate = true )]
8public class CloneableAttribute : TypeAspect
9{
10
11 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty)>
12 _fieldOrPropertyCannotBeReadOnly =
13 new("CLONE01", Severity.Error,
14 "The {0} '{1}' cannot be read-only because it is marked as a [Child].");
15
16 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty, IType)>
17 _missingCloneMethod =
18 new("CLONE02", Severity.Error,
19 "The {0} '{1}' cannot be a [Child] because its type '{2}' does not have a 'Clone' parameterless method.");
20
21 private static readonly DiagnosticDefinition<IMethod> _cloneMethodMustBePublic =
22 new("CLONE03", Severity.Error,
23 "The '{0}' method must be public or internal.");
24
25 private static readonly DiagnosticDefinition<IProperty> _childPropertyMustBeAutomatic =
26 new("CLONE04", Severity.Error,
27 "The property '{0}' cannot be a [Child] because is not an automatic property.");
28
29 public override void BuildAspect( IAspectBuilder<INamedType> builder )
30 {
31 // Verify child fields and properties.
32 if ( !this.VerifyFieldsAndProperties( builder ) )
33 {
34 builder.SkipAspect();
35 return;
36 }
37
38 // Introduce the Clone method.
39 builder.Advice.IntroduceMethod(
40 builder.Target,
41 nameof(this.CloneImpl),
42 whenExists: OverrideStrategy.Override,
43 args: new { T = builder.Target },
44 buildMethod: m =>
45 {
46 m.Name = "Clone";
47 m.ReturnType = builder.Target;
48 } );
49
50 // Implement the ICloneable interface.
51 builder.Advice.ImplementInterface(
52 builder.Target,
53 typeof(ICloneable),
54 OverrideStrategy.Ignore );
55 }
56
57 private bool VerifyFieldsAndProperties( IAspectBuilder<INamedType> builder )
58 {
59 var success = true;
60
61 // Verify that child fields are valid.
62 foreach ( var fieldOrProperty in GetCloneableFieldsOrProperties( builder.Target ) )
63 {
64 // The field or property must be writable.
65 if ( fieldOrProperty.Writeability != Writeability.All )
66 {
67 builder.Diagnostics.Report(
68 _fieldOrPropertyCannotBeReadOnly.WithArguments( (
69 fieldOrProperty.DeclarationKind,
70 fieldOrProperty) ), fieldOrProperty );
71 success = false;
72 }
73
74 // If it is a field, it must be an automatic property.
75 if ( fieldOrProperty is IProperty property && property.IsAutoPropertyOrField == false )
76 {
77 builder.Diagnostics.Report( _childPropertyMustBeAutomatic.WithArguments( property ),
78 property );
79 success = false;
80 }
81
82 // The type of the field must be cloneable.
83 void ReportMissingMethod()
84 {
85 builder.Diagnostics.Report(
86 _missingCloneMethod.WithArguments( (fieldOrProperty.DeclarationKind,
87 fieldOrProperty,
88 fieldOrProperty.Type) ), fieldOrProperty );
89 }
90
91 if ( fieldOrProperty.Type is not INamedType fieldType )
92 {
93 // The field type is an array, a pointer or another special type, which do not have a Clone method.
94 ReportMissingMethod();
95 success = false;
96 }
97 else
98 {
99 var cloneMethod = fieldType.AllMethods.OfName( "Clone" )
100 .SingleOrDefault( p => p.Parameters.Count == 0 );
101
102 if ( cloneMethod == null )
103 {
104 // There is no Clone method.
105 // If may be implemented by an aspect, but we don't have access to aspects on other types
106 // at design time.
107 if ( !MetalamaExecutionContext.Current.ExecutionScenario.IsDesignTime )
108 {
109 if ( !fieldType.BelongsToCurrentProject ||
110 !fieldType.Enhancements().HasAspect<CloneableAttribute>() )
111 {
112 ReportMissingMethod();
113 success = false;
114 }
115 }
116 }
117 else if ( cloneMethod.Accessibility is not (Accessibility.Public
118 or Accessibility.Internal) )
119 {
120 // If we have a Clone method, it must be public.
121 builder.Diagnostics.Report(
122 _cloneMethodMustBePublic.WithArguments( cloneMethod ), fieldOrProperty );
123 success = false;
124 }
125 }
126 }
127
128 return success;
129 }
130
131
132 private static IEnumerable<IFieldOrProperty> GetCloneableFieldsOrProperties( INamedType type )
133 => type.FieldsAndProperties.Where( f =>
134 f.Attributes.OfAttributeType( typeof(ChildAttribute) ).Any() );
135
136 [Template]
137 public virtual T CloneImpl<[CompileTime] T>()
138 {
139 // This compile-time variable will receive the expression representing the base call.
140 // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
141 // we will call MemberwiseClone (this is the initialization of the pattern).
142 IExpression baseCall;
143
144 if ( meta.Target.Method.IsOverride )
145 {
146 baseCall = (IExpression) meta.Base.Clone();
147 }
148 else
149 {
150 baseCall = (IExpression) meta.This.MemberwiseClone();
151 }
152
153 // Define a local variable of the same type as the target type.
154 var clone = (T) baseCall.Value!;
155
156 // Select cloneable fields.
157 var cloneableFields = GetCloneableFieldsOrProperties( meta.Target.Type );
158
159 foreach ( var field in cloneableFields )
160 {
161 // Check if we have a public method 'Clone()' for the type of the field.
162 var fieldType = (INamedType) field.Type;
163
164 field.With( clone ).Value = meta.Cast( fieldType, field.Value?.Clone() );
165 }
166
167 return clone;
168 }
169
170 [InterfaceMember( IsExplicit = true )]
171 private object Clone() => meta.This.Clone();
172}
The first thing to do is to define the errors we want to report as static fields.
10
11 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty)>
12 _fieldOrPropertyCannotBeReadOnly =
13 new("CLONE01", Severity.Error,
14 "The {0} '{1}' cannot be read-only because it is marked as a [Child].");
15
16 private static readonly DiagnosticDefinition<(DeclarationKind, IFieldOrProperty, IType)>
17 _missingCloneMethod =
18 new("CLONE02", Severity.Error,
19 "The {0} '{1}' cannot be a [Child] because its type '{2}' does not have a 'Clone' parameterless method.");
20
21 private static readonly DiagnosticDefinition<IMethod> _cloneMethodMustBePublic =
22 new("CLONE03", Severity.Error,
23 "The '{0}' method must be public or internal.");
24
25 private static readonly DiagnosticDefinition<IProperty> _childPropertyMustBeAutomatic =
26 new("CLONE04", Severity.Error,
27 "The property '{0}' cannot be a [Child] because is not an automatic property.");
For details about reporting errors, see Reporting and suppressing diagnostics.
Then, we add the VerifyFieldsAndProperties
method and call it from BuildAspect
.
57 private bool VerifyFieldsAndProperties( IAspectBuilder<INamedType> builder )
58 {
59 var success = true;
60
61 // Verify that child fields are valid.
62 foreach ( var fieldOrProperty in GetCloneableFieldsOrProperties( builder.Target ) )
63 {
64 // The field or property must be writable.
65 if ( fieldOrProperty.Writeability != Writeability.All )
66 {
67 builder.Diagnostics.Report(
68 _fieldOrPropertyCannotBeReadOnly.WithArguments( (
69 fieldOrProperty.DeclarationKind,
70 fieldOrProperty) ), fieldOrProperty );
71 success = false;
72 }
73
74 // If it is a field, it must be an automatic property.
75 if ( fieldOrProperty is IProperty property && property.IsAutoPropertyOrField == false )
76 {
77 builder.Diagnostics.Report( _childPropertyMustBeAutomatic.WithArguments( property ),
78 property );
79 success = false;
80 }
81
82 // The type of the field must be cloneable.
83 void ReportMissingMethod()
84 {
85 builder.Diagnostics.Report(
86 _missingCloneMethod.WithArguments( (fieldOrProperty.DeclarationKind,
87 fieldOrProperty,
88 fieldOrProperty.Type) ), fieldOrProperty );
89 }
90
91 if ( fieldOrProperty.Type is not INamedType fieldType )
92 {
93 // The field type is an array, a pointer or another special type, which do not have a Clone method.
94 ReportMissingMethod();
95 success = false;
96 }
97 else
98 {
99 var cloneMethod = fieldType.AllMethods.OfName( "Clone" )
100 .SingleOrDefault( p => p.Parameters.Count == 0 );
101
102 if ( cloneMethod == null )
103 {
104 // There is no Clone method.
105 // If may be implemented by an aspect, but we don't have access to aspects on other types
106 // at design time.
107 if ( !MetalamaExecutionContext.Current.ExecutionScenario.IsDesignTime )
108 {
109 if ( !fieldType.BelongsToCurrentProject ||
110 !fieldType.Enhancements().HasAspect<CloneableAttribute>() )
111 {
112 ReportMissingMethod();
113 success = false;
114 }
115 }
116 }
117 else if ( cloneMethod.Accessibility is not (Accessibility.Public
118 or Accessibility.Internal) )
119 {
120 // If we have a Clone method, it must be public.
121 builder.Diagnostics.Report(
122 _cloneMethodMustBePublic.WithArguments( cloneMethod ), fieldOrProperty );
123 success = false;
124 }
125 }
126 }
127
128 return success;
129 }
When we detect an unsupported situation, we report the error using the Report method. The first argument is the diagnostic constructed from the definition stored in the static field, and the second is the invalid field or property.
The third verification requires additional discussion. Our aspect requires the type of child fields or properties to
have a Clone
method. This method can be defined in three ways: in source code (i.e., hand-written), in a referenced
assembly (compiled), or introduced by the Cloneable
aspect itself. In the latter case, the Clone
method may not yet
be present in the code model because the child field type may not have been processed yet. Therefore, if we don't find
the Clone
method, we should check if the child type has the Cloneable
aspect. This aspect can be added as a custom
attribute which we could check using the code model, but it could also be added as a fabric without the help of a custom
attribute. Thus, we must check the presence of the aspect, not the custom attribute. You can check the presence of the
aspect using fieldType.Enhancements().HasAspect<CloneableAttribute>()
. The problem is that, at design time (inside the
IDE), Metalama only knows aspects applied to the current type and its parent types. Metalama uses that strategy for
performance reasons to avoid recompiling the whole assembly at each keystroke. Therefore, that verification cannot be
performed at design time and must be skipped.
Summary
Instead of generating invalid code and confusing the user, our aspect now reports errors when it detects unsupported
situations. It still lacks a mechanism to support anomalies. What if the Game
class includes a collection of Player
s
instead of just one?