Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Building IDE Interactions / Offering Code Fixes & Refactorings

Offering Code Fixes & Refactorings

Attaching code fixes to diagnostics

Whenever an aspect or fabric reports a diagnostic, it can attach a set of code fixes to this diagnostic by calling the IDiagnostic.WithCodeFixes method. To create one-step code fixes, you can use the CodeFixFactory class.

Suggesting code refactorings without diagnostics

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

Example

The following example shows 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 uses the Suggest method to add a code fix suggestion for all properties that are not yet annotated with [NotToString].

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Code.SyntaxBuilders;
using Metalama.Framework.CodeFixes;
using System;
using System.Linq;

namespace Doc.ToStringWithSimpleToString
{
    [AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )]
    public class NotToStringAttribute : Attribute { }

    [LiveTemplate]
    public class ToStringAttribute : TypeAspect
    {
        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            base.BuildAspect( builder );

            // For each field, suggest a code fix to remove from ToString.
            foreach ( var field in builder.Target.FieldsAndProperties.Where( f => !f.IsStatic ) )
            {
                if ( !field.Attributes.Any( a => a.Type.Is( typeof(NotToStringAttribute) ) ) )
                {
                    builder.Diagnostics.Suggest( CodeFixFactory.AddAttribute( field, typeof(NotToStringAttribute), "Exclude from [ToString]" ), field );
                }
            }
        }

        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
        public string IntroducedToString()
        {
            var stringBuilder = new InterpolatedStringBuilder();
            stringBuilder.AddText( "{ " );
            stringBuilder.AddText( meta.Target.Type.Name );
            stringBuilder.AddText( " " );

            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsStatic ).ToList();

            var i = meta.CompileTime( 0 );

            foreach ( var field in fields )
            {
                if ( field.Attributes.Any( a => a.Type.Is( typeof(NotToStringAttribute) ) ) )
                {
                    continue;
                }

                if ( i > 0 )
                {
                    stringBuilder.AddText( ", " );
                }

                stringBuilder.AddText( field.Name );
                stringBuilder.AddText( "=" );
                stringBuilder.AddExpression( field.Invokers.Final.GetValue( meta.This ) );

                i++;
            }

            stringBuilder.AddText( " }" );

            return stringBuilder.ToValue();
        }
    }
}
using System;

namespace Doc.ToStringWithSimpleToString
{
    [ToString]
    internal class MovingVertex
    {
        public double X;

        public double Y;

        public double DX;

        public double DY { get; set; }

        public double Velocity => Math.Sqrt( (this.DX * this.DX) + (this.DY * this.DY) );
    }
}
using System;

namespace Doc.ToStringWithSimpleToString
{
    [ToString]
    internal class MovingVertex
    {
        public double X;

        public double Y;

        public double DX;

        public double DY { get; set; }

        public double Velocity => Math.Sqrt((this.DX * this.DX) + (this.DY * this.DY));

        public override string ToString()
        {
            return $"{{ MovingVertex X={X}, Y={Y}, DX={DX}, DY={DY}, Velocity={Velocity} }}";
        }
    }
}

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, as displayed to the user, and
  • a delegate of type Func<ICodeActionBuilder, Task> that applies the code fix when it is selected by the user.

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

The delegate will typically use one of 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 to 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

We are continuing the previous example, 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 to source code. That is, apply the aspect to the source code itself.

The custom does the following:

  • Apply the aspect itself using ApplyAspectAsync.
  • Remove the [ToString] custom attribute.
  • Remove the [NotToString] custom attributes.
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Code.SyntaxBuilders;
using Metalama.Framework.CodeFixes;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace Doc.ToStringWithComplexCodeFix
{
    [AttributeUsage( AttributeTargets.Field | AttributeTargets.Property )]
    [RunTimeOrCompileTime]
    public class NotToStringAttribute : Attribute { }

    [LiveTemplate]
    public class ToStringAttribute : TypeAspect
    {
        public override void BuildAspect( IAspectBuilder<INamedType> builder )
        {
            base.BuildAspect( builder );

            // Suggest to switch to manual implementation.
            if ( builder.AspectInstance.Predecessors[0].Instance is IAttribute attribute )
            {
                builder.Diagnostics.Suggest(
                    new CodeFix( "Switch to manual implementation", codeFixBuilder => this.ImplementManually( codeFixBuilder, builder.Target ) ),
                    attribute );
            }
        }

        [CompileTime]
        private async Task ImplementManually( ICodeActionBuilder builder, INamedType targetType )
        {
            await builder.ApplyAspectAsync( targetType, this );
            await builder.RemoveAttributesAsync( targetType, typeof(ToStringAttribute) );
            await builder.RemoveAttributesAsync( targetType, typeof(NotToStringAttribute) );
        }

        [Introduce( WhenExists = OverrideStrategy.Override, Name = "ToString" )]
        public string IntroducedToString()
        {
            var stringBuilder = new InterpolatedStringBuilder();
            stringBuilder.AddText( "{ " );
            stringBuilder.AddText( meta.Target.Type.Name );
            stringBuilder.AddText( " " );

            var fields = meta.Target.Type.FieldsAndProperties.Where( f => !f.IsStatic ).ToList();

            var i = meta.CompileTime( 0 );

            foreach ( var field in fields )
            {
                if ( field.Attributes.Any( a => a.Type.Is( typeof(NotToStringAttribute) ) ) )
                {
                    continue;
                }

                if ( i > 0 )
                {
                    stringBuilder.AddText( ", " );
                }

                stringBuilder.AddText( field.Name );
                stringBuilder.AddText( "=" );
                stringBuilder.AddExpression( field.Invokers.Final.GetValue( meta.This ) );

                i++;
            }

            stringBuilder.AddText( " }" );

            return stringBuilder.ToValue();
        }
    }
}
using System;

namespace Doc.ToStringWithComplexCodeFix
{
    [ToString]
    internal class MovingVertex
    {
        public double X;

        public double Y;

        public double DX;

        public double DY { get; set; }

        [NotToString]
        public double Velocity => Math.Sqrt( (this.DX * this.DX) + (this.DY * this.DY) );
    }
}
using System;

namespace Doc.ToStringWithComplexCodeFix
{
    [ToString]
    internal class MovingVertex
    {
        public double X;

        public double Y;

        public double DX;

        public double DY { get; set; }

        [NotToString]
        public double Velocity => Math.Sqrt((this.DX * this.DX) + (this.DY * this.DY));

        public override string ToString()
        {
            return $"{{ MovingVertex X={X}, Y={Y}, DX={DX}, DY={DY} }}";
        }
    }
}

Performance considerations

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

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

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