Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Defining the Eligibility of Aspects

Defining the Eligibility of Aspects

Most of the aspects are designed and implemented for specific kinds of target declarations. For instance, you may decide that your caching aspect will not support void methods or methods with an out or ref parameter. It is important, as the author of the aspect, to make sure that the user of your aspect applies it only to the declarations that you expect. Otherwise, at best, the aspect will cause build errors and confuse the user or at worse, the run-time behavior of your aspect will be incorrect.

Benefits

Defining the eligibility of an aspect has the following benefits:

  • Predictable behavior. Applying an aspect to a declaration for which it was not designed or tested can be a very confusing experience for your users because of error messages they may not understand. It is your responsibility, as the author of the aspect, to ensure that using your aspect is easy and predictable.
  • Standard error messages. All eligibility error messages are standard. It is easier for the aspect users.
  • Relevant suggestions in the IDE. The IDE will only propose code action in the refactoring menu for eligible declarations.

Defining eligibility

To define the eligibility of your aspect, implement or override the BuildEligibility method of the aspect. Use the builder parameter, of type IEligibilityBuilder<T>, to specify the requirements of your aspect. For instance, use builder.MustBeNonAbstract() to require a non-abstract method.

A number of predefined eligibility conditions are implemented by the EligibilityExtensions static class. You can add a custom eligibility condition by calling MustSatisfy and providing your own lambda expression. This method also expects the user-readable string that should be included in the error message when the user attempts to add the aspect to an ineligible declaration.

Note

Your implementation of BuildEligibility must not reference any instance member of the class. Indeed, this method is called on an instance obtained using FormatterServices.GetUninitializedObject that is, without invoking the class constructor.

Example

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Eligibility;

namespace Doc.Eligibility
{
    internal class LogAttribute : OverrideMethodAspect
    {
        public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
        {
            base.BuildEligibility( builder );

            // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
            builder.MustBeNonStatic();
        }

        public override dynamic? OverrideMethod()
        {
            meta.This.logger.WriteLine( $"Executing {meta.Target.Method}" );

            return meta.Proceed();
        }
    }
}
using System;
using System.IO;

namespace Doc.Eligibility
{
    internal class SomeClass
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void InstanceMethod() { }

        [Log]
        private static void StaticMethod() { }
    }
}
// Error LAMA0037 on `Log`: `The aspect 'Log' cannot be applied to 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must be a non-static method.`

When to emit custom errors instead?

It may be tempting to add an eligibility condition for every requirement of your aspect instead of emitting a custom error message. However, this may be confusing for the user.

As a rule of thumb, you should use eligibility to define those declarations for which it makes sense to either apply the aspect or not, and use error messages when the aspect makes sense on the declaration, but there is some contingency that prevents the aspect from being used.

For details about reporting errors, see Reporting and Suppressing Diagnostics.

Example 1

Adding a caching to a void method does not make sense and should be addressed with eligibility. However, the fact that your aspect does not support methods returning a collection is an implementation detail and should be reported using a custom error.

Example 2

Adding a dependency injection aspect to an int or string field does not make sense and this condition should therefore be expressed using the eligibility API. However, the fact that your implementation of the aspect requires the field to be non-read-only is a contingency and should be reported as an error.

Example

The following example expands the previous one, reporting custom errors when the target class does not define a field logger of type TextWriter.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using Metalama.Framework.Eligibility;
using System.IO;
using System.Linq;

namespace Doc.EligibilityAndValidation
{
    internal class LogAttribute : OverrideMethodAspect
    {
        private static readonly DiagnosticDefinition<INamedType> _error1 = new("MY001", Severity.Error,
            "The type '{0}' must have a field named '_logger'.");

        private static readonly DiagnosticDefinition<IField> _error2 = new("MY002", Severity.Error,
            "The type of the field '{0}' must be 'TextWriter'.");

        public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
        {
            base.BuildEligibility( builder );

            // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
            builder.MustBeNonStatic();
        }

        public override void BuildAspect( IAspectBuilder<IMethod> builder )
        {
            base.BuildAspect( builder );

            // Validate that the target file has a field named 'logger' of type TextWriter.
            var declaringType = builder.Target.DeclaringType;
            var loggerField = declaringType.Fields.OfName( "_logger" ).SingleOrDefault();

            if ( loggerField == null )
            {
                builder.Diagnostics.Report( _error1.WithArguments( declaringType ), declaringType );
                builder.SkipAspect();
            }
            else if ( !loggerField.Type.Is( typeof(TextWriter) ) )
            {
                builder.Diagnostics.Report( _error2.WithArguments( loggerField ), loggerField );
                builder.SkipAspect();
            }
        }

        public override dynamic? OverrideMethod()
        {
            meta.This.logger.WriteLine( $"Executing {meta.Target.Method}" );

            return meta.Proceed();
        }
    }
}
namespace Doc.EligibilityAndValidation
{
    internal class SomeClass
    {
        private object? _logger;

        [Log]
        private void InstanceMethod() { }

        [Log]
        private static void StaticMethod() { }
    }
}
// Error MY002 on `_logger`: `The type of the field 'SomeClass._logger' must be 'TextWriter'.`
// Error LAMA0037 on `Log`: `The aspect 'Log' cannot be applied to 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must be a non-static method.`