Attaching code fixes to diagnostics
To attach a code fix to a diagnostic:
- Add the
Metalama.Extensions.CodeFixespackage. - After creating the diagnostic, call the IDiagnostic.WithCodeFixes extension method.
- Use the CodeFixFactory class to create predefined, single-step code fixes, such as adding or removing a custom attribute. For more complex code fixes, see Offering code fixes and refactorings.
Suggesting refactorings without diagnostics
Aspects and fabrics can suggest code refactorings without reporting diagnostics by calling the Suggest method.
Example: code fix without diagnostic
The following example demonstrates an aspect that implements the ToString method. By default, it includes all public properties of the class in the ToString result. However, developers can opt out by adding [NotToString] to any property.
The aspect uses the Suggest method to add a code fix suggestion for all properties not yet annotated with [NotToString].
1using Metalama.Extensions.CodeFixes;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
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( f => f is
22 {
23 IsStatic: false, IsImplicitlyDeclared: false
24 } ) )
25 {
26 if ( !field.Attributes.Any( a => a.Type.IsConvertibleTo(
27 typeof(NotToStringAttribute) ) ) )
28 {
29 builder.Diagnostics.Suggest(
30 CodeFixFactory.AddAttribute(
31 field,
32 typeof(NotToStringAttribute),
33 "Exclude from [ToString]" ),
34 field );
35 }
36 }
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
48 .Where( f => f is { IsImplicitlyDeclared: false, IsStatic: false } )
49 .ToList();
50
51 var i = meta.CompileTime( 0 );
52
53 foreach ( var field in fields )
54 {
55 if ( field.Attributes.Any( a => a.Type.IsConvertibleTo(
56 typeof(NotToStringAttribute) ) ) )
57 {
58 continue;
59 }
60
61 if ( i > 0 )
62 {
63 stringBuilder.AddText( ", " );
64 }
65
66 stringBuilder.AddText( field.Name );
67 stringBuilder.AddText( "=" );
68 stringBuilder.AddExpression( field );
69
70 i++;
71 }
72
73 stringBuilder.AddText( " }" );
74
75 return stringBuilder.ToValue();
76 }
77}
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 custom 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 is displayed to the user
- A delegate of type
Func<ICodeActionBuilder, Task>that applies the code fix when the user selects it
The title must be globally unique for the target declaration. Even two different aspects can't provide two code fixes with the same title to the same declaration.
The delegate typically uses 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: custom code fix
The previous example continues here, but instead of a single-step code fix, we offer users 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.Extensions.CodeFixes;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Code.SyntaxBuilders;
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( a => a.Type.IsConvertibleTo(
58 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 are ignored. To avoid generating code fixes at compile time, make your logic conditional upon the
MetalamaExecutionContext.Current.ExecutionScenario.CapturesCodeFixTitlesexpression.The
Func<ICodeActionBuilder, Task>delegate is only executed when you select the code fix or refactoring. However, the entire aspect is executed again, which has two implications:- The logic that creates the delegate must be highly efficient because it's rarely used. Move any expensive logic to the implementation of the delegate itself.
- To avoid generating the delegate, make it conditional upon the
MetalamaExecutionContext.Current.ExecutionScenario.CapturesCodeFixImplementationsexpression.
At design time, all code fix titles, including those added by the Suggest method, are cached for the complete solution. Therefore, avoid adding a large number of suggestions. The current Metalama design isn't suited for this scenario.