Defining the eligibility of aspects
Most 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 out
or ref
parameters. As the author of the aspect, it is essential that you make sure that users of your aspect apply it only to the declarations that you expect. Otherwise, the aspect will cause build errors with confusing messages or even incorrect run-time behavior.
Benefits
Defining the eligibility of an aspect has the following benefits:
- Predictable behavior. Applying an aspect to a declaration the aspect was not designed or tested for can be a very confusing experience for your users because of error messages they may not understand. As the author of the aspect, it is your responsibility to ensure that using your aspect is easy and predictable.
- Standard error messages. All eligibility error messages are standard. They are easier to understand for 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, which is of type IEligibilityBuilder<T>, to specify the requirements of your aspect. For instance, use builder.MustNotBeAbstract() to require a non-abstract method.
Several predefined eligibility conditions are implemented by the EligibilityExtensions static class. You can add a custom eligibility condition by calling MustSatisfy and by providing a 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
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Eligibility;
4
5namespace Doc.Eligibility
6{
7 internal class LogAttribute : OverrideMethodAspect
8 {
9 public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
10 {
11 base.BuildEligibility( builder );
12
13 // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
14 builder.MustNotBeStatic();
15 }
16
17 public override dynamic? OverrideMethod()
18 {
19 meta.This._logger.WriteLine( $"Executing {meta.Target.Method}" );
20
21 return meta.Proceed();
22 }
23 }
24}
1using System;
2using System.IO;
3
4namespace Doc.Eligibility
5{
6 internal class SomeClass
7 {
8 private TextWriter _logger = Console.Out;
9
10 [Log]
11 private void InstanceMethod() { }
12
Error LAMA0037: The aspect 'Log' cannot be applied to the method 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must not be static.
13 [Log]
14 private static void StaticMethod() { }
15 }
16}
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 apply the aspect or not and use error messages when the aspect makes sense on the declaration. Still, some contingency may prevent the aspect from being used, and this is where you should report errors.
For details about reporting errors, see Reporting and suppressing diagnostics.
Example 1
Adding a caching aspect 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 a limitation caused by your particular implementation 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 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 3
The following example expands the previous one, reporting custom errors when the target class does not define a field logger
of type TextWriter
.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using Metalama.Framework.Eligibility;
5using System.IO;
6using System.Linq;
7
8namespace Doc.EligibilityAndValidation
9{
10 internal class LogAttribute : OverrideMethodAspect
11 {
12 private static readonly DiagnosticDefinition<INamedType> _error1 = new(
13 "MY001",
14 Severity.Error,
15 "The type '{0}' must have a field named '_logger'." );
16
17 private static readonly DiagnosticDefinition<IField> _error2 = new(
18 "MY002",
19 Severity.Error,
20 "The type of the field '{0}' must be 'TextWriter'." );
21
22 public override void BuildEligibility( IEligibilityBuilder<IMethod> builder )
23 {
24 base.BuildEligibility( builder );
25
26 // The aspect must not be offered to non-static methods because it uses a static field 'logger'.
27 builder.MustNotBeStatic();
28 }
29
30 public override void BuildAspect( IAspectBuilder<IMethod> builder )
31 {
32 base.BuildAspect( builder );
33
34 // Validate that the target file has a field named 'logger' of type TextWriter.
35 var declaringType = builder.Target.DeclaringType;
36 var loggerField = declaringType.Fields.OfName( "_logger" ).SingleOrDefault();
37
38 if ( loggerField == null )
39 {
40 builder.Diagnostics.Report( _error1.WithArguments( declaringType ), declaringType );
41 builder.SkipAspect();
42 }
43 else if ( !loggerField.Type.Is( typeof(TextWriter) ) )
44 {
45 builder.Diagnostics.Report( _error2.WithArguments( loggerField ), loggerField );
46 builder.SkipAspect();
47 }
48 }
49
50 public override dynamic? OverrideMethod()
51 {
52 meta.This.logger.WriteLine( $"Executing {meta.Target.Method}" );
53
54 return meta.Proceed();
55 }
56 }
57}
1namespace Doc.EligibilityAndValidation
2{
3 internal class SomeClass
4 {
Error MY002: The type of the field 'SomeClass._logger' must be 'TextWriter'.
5 private object? _logger;
6
7 [Log]
8 private void InstanceMethod() { }
9
Error LAMA0037: The aspect 'Log' cannot be applied to the method 'SomeClass.StaticMethod()' because 'SomeClass.StaticMethod()' must not be static.
10 [Log]
11 private static void StaticMethod() { }
12 }
13}