Open sandboxFocusImprove this doc

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
8    [Child]
    Error CLONE01: The property 'Game.Settings' cannot be read-only because it is marked as a [Child].

9    public GameSettings Settings { get; }
10}
11
12[Cloneable]
13internal class GameSettings
14{
15    public int Level { get; set; }
16
17    public string World { get; set; }
18}
19
20internal class Player
21{
22    public string Name { get; }
23}

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{
6    [Child]
    Error CLONE01: The property 'Game.Player' cannot be read-only because it is marked as a [Child].

7    public Player Player { get; }
8
9    [Child]
10    public GameSettings Settings { get; private set; }
11}
12
13[Cloneable]
14internal class GameSettings
15{
16    public int Level { get; set; }
17
18    public string World { get; set; }
19}
20
21internal class Player
22{
23    public string Name { get; }
24}

The Clone method must be public or internal.

1namespace ErrorProtectedCloneMethod;
2
3[Cloneable]
4internal class Game
5{
6    [Child]
    Error CLONE03: The 'Player.Clone()' method must be public or internal.

7    public Player Player { get; private set; }
8}
9
10internal class Player
11{
12    public string Name { get; init; }
13
14    protected Player Clone() => new() { Name = this.Name };
15}

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
28    public string World { get; set; }
29}
30
31internal class Player
32{
33    public string Name { get; }
34}

Aspect implementation

The full updated aspect code is here:

1using JetBrains.Annotations;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using Metalama.Framework.Project;
6
7[Inheritable]
8[EditorExperience( SuggestAsLiveTemplate = true )]
9public class CloneableAttribute : TypeAspect
10{
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    // 
39
40    public override void BuildAspect( IAspectBuilder<INamedType> builder )
41    {
42        // Verify child fields and properties.
43        if ( !this.VerifyFieldsAndProperties( builder ) )
44        {
45            builder.SkipAspect();
46
47            return;
48        }
49
50        // Introduce the Clone method.
51        builder.IntroduceMethod(
52            nameof(this.CloneImpl),
53            whenExists: OverrideStrategy.Override,
54            args: new { T = builder.Target },
55            buildMethod: m =>
56            {
57                m.Name = "Clone";
58                m.ReturnType = builder.Target;
59            } );
60
61        // Implement the ICloneable interface.
62        builder.ImplementInterface(
63            typeof(ICloneable),
64            OverrideStrategy.Ignore );
65    }
66
67    private bool VerifyFieldsAndProperties( IAspectBuilder<INamedType> builder )
68    {
69        var success = true;
70
71        // Verify that child fields are valid.
72        foreach ( var fieldOrProperty in GetCloneableFieldsOrProperties( builder.Target ) )
73        {
74            // The field or property must be writable.
75            if ( fieldOrProperty.Writeability != Writeability.All )
76            {
77                builder.Diagnostics.Report(
78                    _fieldOrPropertyCannotBeReadOnly.WithArguments(
79                        (
80                            fieldOrProperty.DeclarationKind,
81                            fieldOrProperty) ),
82                    fieldOrProperty );
83
84                success = false;
85            }
86
87            // If it is a field, it must be an automatic property.
88            if ( fieldOrProperty is IProperty { IsAutoPropertyOrField: false } property )
89            {
90                builder.Diagnostics.Report(
91                    _childPropertyMustBeAutomatic.WithArguments( property ),
92                    property );
93
94                success = false;
95            }
96
97            // The type of the field must be cloneable.
98            void ReportMissingMethod()
99            {
100                builder.Diagnostics.Report(
101                    _missingCloneMethod.WithArguments(
102                        (fieldOrProperty.DeclarationKind,
103                         fieldOrProperty,
104                         fieldOrProperty.Type) ),
105                    fieldOrProperty );
106            }
107
108            if ( fieldOrProperty.Type is not INamedType fieldType )
109            {
110                // The field type is an array, a pointer or another special type, which do not have a Clone method.
111                ReportMissingMethod();
112                success = false;
113            }
114            else
115            {
116                var cloneMethod = fieldType.AllMethods.OfName( "Clone" )
117                    .SingleOrDefault( p => p.Parameters.Count == 0 );
118
119                if ( cloneMethod == null )
120                {
121                    // There is no Clone method.
122                    // It may be implemented by an aspect, but we don't have access to aspects on other types
123                    // at design time.
124                    if ( !MetalamaExecutionContext.Current.ExecutionScenario.IsDesignTime )
125                    {
126                        if ( !fieldType.BelongsToCurrentProject ||
127                             !fieldType.Enhancements().HasAspect<CloneableAttribute>() )
128                        {
129                            ReportMissingMethod();
130                            success = false;
131                        }
132                    }
133                }
134                else if ( cloneMethod.Accessibility is not (Accessibility.Public
135                         or Accessibility.Internal) )
136                {
137                    // If we have a Clone method, it must be public.
138                    builder.Diagnostics.Report(
139                        _cloneMethodMustBePublic.WithArguments( cloneMethod ),
140                        fieldOrProperty );
141
142                    success = false;
143                }
144            }
145        }
146
147        return success;
148    }
149
150    private static IEnumerable<IFieldOrProperty> GetCloneableFieldsOrProperties( INamedType type )
151        => type.FieldsAndProperties.Where( f =>
152                                               f.Attributes.OfAttributeType( typeof(ChildAttribute) ).Any() );
153
154    [Template]
155    public virtual T CloneImpl<[CompileTime] T>()
156    {
157        // This compile-time variable will receive the expression representing the base call.
158        // If we have a public Clone method, we will use it (this is the chaining pattern). Otherwise,
159        // we will call MemberwiseClone (this is the initialization of the pattern).
160        IExpression baseCall;
161
162        if ( meta.Target.Method.IsOverride )
163        {
164            baseCall = (IExpression) meta.Base.Clone();
165        }
166        else
167        {
168            baseCall = (IExpression) meta.This.MemberwiseClone();
169        }
170
171        // Define a local variable of the same type as the target type.
172        var clone = (T) baseCall.Value!;
173
174        // Select cloneable fields.
175        var cloneableFields = GetCloneableFieldsOrProperties( meta.Target.Type );
176
177        foreach ( var field in cloneableFields )
178        {
179            // Check if we have a public method 'Clone()' for the type of the field.
180            var fieldType = (INamedType) field.Type;
181
182            field.WithObject( clone ).Value = meta.Cast( fieldType, field.Value?.Clone() );
183        }
184
185        return clone;
186    }
187
188    [InterfaceMember( IsExplicit = true )]
189    [UsedImplicitly]
190    private object Clone() => meta.This.Clone();
191}

The first thing to do is to define the errors we want to report as static fields.

12private 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
19private 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
26private static readonly DiagnosticDefinition<IMethod> _cloneMethodMustBePublic =
27    new(
28        "CLONE03",
29        Severity.Error,
30        "The '{0}' method must be public or internal." );
31
32private 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

For details about reporting errors, see Reporting and suppressing diagnostics.

Then, we add the VerifyFieldsAndProperties method and call it from BuildAspect.

67private bool VerifyFieldsAndProperties( IAspectBuilder<INamedType> builder )
68{
69    var success = true;
70
71    // Verify that child fields are valid.
72    foreach ( var fieldOrProperty in GetCloneableFieldsOrProperties( builder.Target ) )
73    {
74        // The field or property must be writable.
75        if ( fieldOrProperty.Writeability != Writeability.All )
76        {
77            builder.Diagnostics.Report(
78                _fieldOrPropertyCannotBeReadOnly.WithArguments(
79                    (
80                        fieldOrProperty.DeclarationKind,
81                        fieldOrProperty) ),
82                fieldOrProperty );
83
84            success = false;
85        }
86
87        // If it is a field, it must be an automatic property.
88        if ( fieldOrProperty is IProperty { IsAutoPropertyOrField: false } property )
89        {
90            builder.Diagnostics.Report(
91                _childPropertyMustBeAutomatic.WithArguments( property ),
92                property );
93
94            success = false;
95        }
96
97        // The type of the field must be cloneable.
98        void ReportMissingMethod()
99        {
100            builder.Diagnostics.Report(
101                _missingCloneMethod.WithArguments(
102                    (fieldOrProperty.DeclarationKind,
103                     fieldOrProperty,
104                     fieldOrProperty.Type) ),
105                fieldOrProperty );
106        }
107
108        if ( fieldOrProperty.Type is not INamedType fieldType )
109        {
110            // The field type is an array, a pointer or another special type, which do not have a Clone method.
111            ReportMissingMethod();
112            success = false;
113        }
114        else
115        {
116            var cloneMethod = fieldType.AllMethods.OfName( "Clone" )
117                .SingleOrDefault( p => p.Parameters.Count == 0 );
118
119            if ( cloneMethod == null )
120            {
121                // There is no Clone method.
122                // It may be implemented by an aspect, but we don't have access to aspects on other types
123                // at design time.
124                if ( !MetalamaExecutionContext.Current.ExecutionScenario.IsDesignTime )
125                {
126                    if ( !fieldType.BelongsToCurrentProject ||
127                         !fieldType.Enhancements().HasAspect<CloneableAttribute>() )
128                    {
129                        ReportMissingMethod();
130                        success = false;
131                    }
132                }
133            }
134            else if ( cloneMethod.Accessibility is not (Accessibility.Public
135                     or Accessibility.Internal) )
136            {
137                // If we have a Clone method, it must be public.
138                builder.Diagnostics.Report(
139                    _cloneMethodMustBePublic.WithArguments( cloneMethod ),
140                    fieldOrProperty );
141
142                success = false;
143            }
144        }
145    }
146
147    return success;
148}
149

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 Players instead of just one?