MetalamaConceptual documentationCreating aspectsBuilding IDE interactionsOffering code fixes & refactorings
Open sandboxFocusImprove this doc

Offering code fixes and refactorings

Attaching code fixes to diagnostics

When an aspect or fabric reports a diagnostic, it can attach a set of code fixes to this diagnostic by invoking the IDiagnostic.WithCodeFixes method. The CodeFixFactory class can be used to create single-step code fixes.

Suggesting code refactorings without diagnostics

An aspect or fabric can also suggest a code refactoring without reporting a diagnostic by invoking the Suggest method.

Example

The example below demonstrates an aspect that implements the ToString method. By default, it includes all public properties of the class in the ToString result. However, the developer using the aspect can opt-out by adding [NotToString] to any property.

The aspect utilizes the Suggest method to add a code fix suggestion for all properties not yet annotated with [NotToString].

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using Metalama.Framework.CodeFixes;
5using System;
6using System.Linq;
7
8namespace Doc.ToStringWithSimpleToString
9{
10    [AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )]
11    public class NotToStringAttribute : Attribute { }
12
13    [EditorExperience( SuggestAsLiveTemplate = true )]
14    public class ToStringAttribute : TypeAspect
15    {
16        public override void BuildAspect( IAspectBuilder<INamedType> builder )
17        {
18            base.BuildAspect( builder );
19
20            // For each field, suggest a code fix to remove from ToString.
21            foreach ( var field in builder.Target.FieldsAndProperties.Where( f => !f.IsStatic && !f.IsImplicitlyDeclared ) )
22            {
23                if ( !field.Attributes.Any( a => a.Type.Is( typeof(NotToStringAttribute) ) ) )
24                {
25                    builder.Diagnostics.Suggest( CodeFixFactory.AddAttribute( field, typeof(NotToStringAttribute), "Exclude from [ToString]" ), field );
26                }
27            }
28        }
29
30        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
31        public string IntroducedToString()
32        {
33            var stringBuilder = new InterpolatedStringBuilder();
34            stringBuilder.AddText( "{ " );
35            stringBuilder.AddText( meta.Target.Type.Name );
36            stringBuilder.AddText( " " );
37
38            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsImplicitlyDeclared && !f.IsStatic ).ToList();
39
40            var i = meta.CompileTime( 0 );
41
42            foreach ( var field in fields )
43            {
44                if ( field.Attributes.Any( a => a.Type.Is( typeof(NotToStringAttribute) ) ) )
45                {
46                    continue;
47                }
48
49                if ( i > 0 )
50                {
51                    stringBuilder.AddText( ", " );
52                }
53
54                stringBuilder.AddText( field.Name );
55                stringBuilder.AddText( "=" );
56                stringBuilder.AddExpression( field );
57
58                i++;
59            }
60
61            stringBuilder.AddText( " }" );
62
63            return stringBuilder.ToValue();
64        }
65    }
66}
Source Code
1using System;
2
3namespace Doc.ToStringWithSimpleToString
4{
5    [ToString]
6    internal class MovingVertex
7    {
8        public double X;
9
10        public double Y;
11
12        public double DX;
13
14        public double DY { get; set; }
15
16        public double Velocity => Math.Sqrt( (this.DX * this.DX) + (this.DY * this.DY) );
17    }
18}
Transformed Code
1using System;
2
3namespace Doc.ToStringWithSimpleToString
4{
5    [ToString]
6    internal class MovingVertex
7    {
8        public double X;
9
10        public double Y;
11
12        public double DX;
13
14        public double DY { get; set; }
15
16        public double Velocity => Math.Sqrt((this.DX * this.DX) + (this.DY * this.DY));
17
18        public override string ToString()
19        {
20            return $"{{ MovingVertex X={X}, Y={Y}, DX={DX}, DY={DY}, Velocity={Velocity} }}";
21        }
22    }
23}

Building multi-step code fixes

To create a custom code fix, instantiate the CodeFix class using the constructor instead of the CodeFixFactory class.

The CodeFix constructor accepts two arguments:

  • The title of the code fix, which will be displayed to the user, and
  • A delegate of type Func<ICodeActionBuilder, Task> which will apply the code fix when the user selects it

The title must be globally unique for the target declaration. Even two different aspects cannot provide two code fixes with the same title to the same declaration.

The delegate will typically utilize one of the following methods of the ICodeActionBuilder interface:

Method Description
AddAttributeAsync Adds a custom attribute to a declaration.
RemoveAttributesAsync Removes all custom attributes of a given type from a given declaration and all contained declarations.
ApplyAspectAsync Transforms the source code using an aspect (as if it were applied as a live template).

Example

The previous example is continued here, but instead of a single-step code fix, we want to offer the user the ability to switch from an aspect-oriented implementation of ToString by applying the aspect to the source code itself.

The custom code fix performs the following actions:

  • Applies the aspect using the ApplyAspectAsync method.
  • Removes the [ToString] custom attribute.
  • Removes the [NotToString] custom attributes.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Code.SyntaxBuilders;
4using Metalama.Framework.CodeFixes;
5using System;
6using System.Linq;
7using System.Threading.Tasks;
8
9namespace Doc.ToStringWithComplexCodeFix
10{
11    [AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )]
12    [RunTimeOrCompileTime]
13    public class NotToStringAttribute : Attribute { }
14
15    [EditorExperience( SuggestAsLiveTemplate = true )]
16    public class ToStringAttribute : TypeAspect
17    {
18        public override void BuildAspect( IAspectBuilder<INamedType> builder )
19        {
20            base.BuildAspect( builder );
21
22            // Suggest to switch to manual implementation.
23            if ( builder.AspectInstance.Predecessors[0].Instance is IAttribute attribute )
24            {
25                builder.Diagnostics.Suggest(
26                    new CodeFix( "Switch to manual implementation", codeFixBuilder => this.ImplementManually( codeFixBuilder, builder.Target ) ),
27                    attribute );
28            }
29        }
30
31        [CompileTime]
32        private async Task ImplementManually( ICodeActionBuilder builder, INamedType targetType )
33        {
34            await builder.ApplyAspectAsync( targetType, this );
35            await builder.RemoveAttributesAsync( targetType, typeof(ToStringAttribute) );
36            await builder.RemoveAttributesAsync( targetType, typeof(NotToStringAttribute) );
37        }
38
39        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
40        public string IntroducedToString()
41        {
42            var stringBuilder = new InterpolatedStringBuilder();
43            stringBuilder.AddText( "{ " );
44            stringBuilder.AddText( meta.Target.Type.Name );
45            stringBuilder.AddText( " " );
46
47            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsStatic && !f.IsImplicitlyDeclared ).ToList();
48
49            var i = meta.CompileTime( 0 );
50
51            foreach ( var field in fields )
52            {
53                if ( field.Attributes.Any( a => a.Type.Is( typeof(NotToStringAttribute) ) ) )
54                {
55                    continue;
56                }
57
58                if ( i > 0 )
59                {
60                    stringBuilder.AddText( ", " );
61                }
62
63                stringBuilder.AddText( field.Name );
64                stringBuilder.AddText( "=" );
65                stringBuilder.AddExpression( field.Value );
66
67                i++;
68            }
69
70            stringBuilder.AddText( " }" );
71
72            return stringBuilder.ToValue();
73        }
74    }
75}
Source Code
1using System;
2
3namespace Doc.ToStringWithComplexCodeFix
4{
5    [ToString]
6    internal class MovingVertex
7    {
8        public double X;
9
10        public double Y;
11
12        public double DX;
13
14        public double DY { get; set; }
15
16        [NotToString]
17        public double Velocity => Math.Sqrt( (this.DX * this.DX) + (this.DY * this.DY) );
18    }
19}
Transformed Code
1using System;
2
3namespace Doc.ToStringWithComplexCodeFix
4{
5    [ToString]
6    internal class MovingVertex
7    {
8        public double X;
9
10        public double Y;
11
12        public double DX;
13
14        public double DY { get; set; }
15
16        [NotToString]
17        public double Velocity => Math.Sqrt((this.DX * this.DX) + (this.DY * this.DY));
18
19        public override string ToString()
20        {
21            return $"{{ MovingVertex X={X}, Y={Y}, DX={DX}, DY={DY} }}";
22        }
23    }
24}

Performance considerations

  • Code fixes and refactorings are only useful at design time. At compile time, all code fixes will be ignored. To avoid generating code fixes at compile time, you can make your logic conditional upon the MetalamaExecutionContext.Current.ExecutionScenario.CapturesCodeFixTitles expression.

  • The Func<ICodeActionBuilder, Task> delegate is only executed when the user selects the code fix or refactoring. However, the entire aspect will be executed again, which has two implications:

    • The logic that creates the delegate must be highly efficient because it is rarely used. Any expensive logic should be moved to the implementation of the delegate itself.
    • To avoid generating the delegate, you can make it conditional upon the MetalamaExecutionContext.Current.ExecutionScenario.CapturesCodeFixImplementations expression.
  • At design time, all code fix titles, including those added by the Suggest method, are cached for the complete solution. Therefore, you should avoid adding a large number of suggestions. The current Metalama design is not suited for this scenario.