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 )]
11public class NotToStringAttribute : Attribute { }
12
13[EditorExperience( SuggestAsLiveTemplate = true )]
14public 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(
22 f => f is { IsStatic: false, IsImplicitlyDeclared: false } ) )
23 {
24 if ( !field.Attributes.Any(
25 a => a.Type.IsConvertibleTo( typeof(NotToStringAttribute) ) ) )
26 {
27 builder.Diagnostics.Suggest(
28 CodeFixFactory.AddAttribute(
29 field,
30 typeof(NotToStringAttribute),
31 "Exclude from [ToString]" ),
32 field );
33 }
34 }
35 }
36
37 [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
38 public string IntroducedToString()
39 {
40 var stringBuilder = new InterpolatedStringBuilder();
41 stringBuilder.AddText( "{ " );
42 stringBuilder.AddText( meta.Target.Type.Name );
43 stringBuilder.AddText( " " );
44
45 var fields = meta.Target.Type.FieldsAndProperties
46 .Where( f => f is { IsImplicitlyDeclared: false, IsStatic: false } )
47 .ToList();
48
49 var i = meta.CompileTime( 0 );
50
51 foreach ( var field in fields )
52 {
53 if ( field.Attributes.Any(
54 a => a.Type.IsConvertibleTo( typeof(NotToStringAttribute) ) ) )
55 {
56 continue;
57 }
58
59 if ( i > 0 )
60 {
61 stringBuilder.AddText( ", " );
62 }
63
64 stringBuilder.AddText( field.Name );
65 stringBuilder.AddText( "=" );
66 stringBuilder.AddExpression( field );
67
68 i++;
69 }
70
71 stringBuilder.AddText( " }" );
72
73 return stringBuilder.ToValue();
74 }
75}
1using System;
2
3namespace Doc.ToStringWithSimpleToString;
4
5[ToString]
6internal 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}
1using System;
2
3namespace Doc.ToStringWithSimpleToString;
4
5[ToString]
6internal 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}
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]
13public class NotToStringAttribute : Attribute { }
14
15[EditorExperience( SuggestAsLiveTemplate = true )]
16public 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(
27 "Switch to manual implementation",
28 codeFixBuilder => this.ImplementManually( codeFixBuilder, builder.Target ) ),
29 attribute );
30 }
31 }
32
33 [CompileTime]
34 private async Task ImplementManually( ICodeActionBuilder builder, INamedType targetType )
35 {
36 await builder.ApplyAspectAsync( targetType, this );
37 await builder.RemoveAttributesAsync( targetType, typeof(ToStringAttribute) );
38 await builder.RemoveAttributesAsync( targetType, typeof(NotToStringAttribute) );
39 }
40
41 [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
42 public string IntroducedToString()
43 {
44 var stringBuilder = new InterpolatedStringBuilder();
45 stringBuilder.AddText( "{ " );
46 stringBuilder.AddText( meta.Target.Type.Name );
47 stringBuilder.AddText( " " );
48
49 var fields = meta.Target.Type.FieldsAndProperties
50 .Where( f => f is { IsStatic: false, IsImplicitlyDeclared: false } )
51 .ToList();
52
53 var i = meta.CompileTime( 0 );
54
55 foreach ( var field in fields )
56 {
57 if ( field.Attributes.Any(
58 a => a.Type.IsConvertibleTo( typeof(NotToStringAttribute) ) ) )
59 {
60 continue;
61 }
62
63 if ( i > 0 )
64 {
65 stringBuilder.AddText( ", " );
66 }
67
68 stringBuilder.AddText( field.Name );
69 stringBuilder.AddText( "=" );
70 stringBuilder.AddExpression( field.Value );
71
72 i++;
73 }
74
75 stringBuilder.AddText( " }" );
76
77 return stringBuilder.ToValue();
78 }
79}
1using System;
2
3namespace Doc.ToStringWithComplexCodeFix;
4
5[ToString]
6internal 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}
1using System;
2
3namespace Doc.ToStringWithComplexCodeFix;
4
5[ToString]
6internal 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}
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.