MetalamaConceptual documentationCreating aspectsValidating code
Open sandboxFocusImprove this doc

Validating code from an aspect

Validating source code and providing meaningful error messages is a critical feature of most aspects. Failure to do so can result in confusing error messages for the aspect's user or even invalid behavior at runtime.

The first two techniques for validating code involve defining eligibility (see Defining the eligibility of aspects) and reporting errors from the BuildAspect method (see Reporting and suppressing diagnostics). In this article, we introduce two additional techniques:

  • Validating the code before applying any aspect, or after applying all aspects.
  • Validating references to the target declaration of the aspect.

Validating code before or after aspects

By default, the BuildAspect receives the version of the code model before applying the current aspect. However, there may be instances where you need to validate a different version of the code model. Metalama allows you to validate three versions:

  • Before the current aspect has been applied,
  • Before any aspect has been applied, or
  • After all aspects have been applied.

To validate a different version of the code model, follow these steps:

  1. Define one or more static fields of type DiagnosticDefinition as explained in Reporting and suppressing diagnostics.
  2. Create a method with the signature void Validate(in DeclarationValidationContext context). Implement the validation logic in this method. All the data you need is in the DeclarationValidationContext object. When you detect a rule violation, report a diagnostic as described in Reporting and suppressing diagnostics.
  3. Override or implement the BuildAspect method of your aspect. From this method:
    1. Access the builder.Outbound property,
    2. Call the AfterAllAspects() or BeforeAnyAspect() method to select the version of the code model,
    3. Select declarations to be validated using the SelectMany and Select methods,
    4. Call the ValidateReferences method and pass a delegate to the validation method.

Example: requiring a later aspect to be applied

The following example demonstrates how to validate that the target type of the Log aspect contains a field named _logger. The implementation allows the _logger field to be introduced after the Log aspect has been applied, thanks to a call to AfterAllAspects().

1using Doc.ValidateAfterAllAspects;
2using Metalama.Framework.Aspects;
3using Metalama.Framework.Code;
4using Metalama.Framework.Diagnostics;
5using Metalama.Framework.Validation;
6using System.IO;
7using System.Linq;
8
9// Note that aspects are applied in inverse order than they appear in the next line.
10[assembly: AspectOrder( typeof(AddLoggerAttribute), typeof(LogAttribute) )]
11
12namespace Doc.ValidateAfterAllAspects
13{
14    internal class LogAttribute : OverrideMethodAspect
15    {
16        private static DiagnosticDefinition<INamedType> _error = new( "MY001", Severity.Error, "The type {0} must have a field named _logger." );
17
18        public override void BuildAspect( IAspectBuilder<IMethod> builder )
19        {
20            builder.Outbound.AfterAllAspects().Select( m => m.DeclaringType ).Validate( this.ValidateDeclaringType );
21        }
22
23        private void ValidateDeclaringType( in DeclarationValidationContext context )
24        {
25            var type = (INamedType) context.Declaration;
26
27            if ( !type.AllFields.OfName( "_logger" ).Any() )
28            {
29                context.Diagnostics.Report( _error.WithArguments( type ) );
30            }
31        }
32
33        public override dynamic? OverrideMethod()
34        {
35            meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );
36
37            return meta.Proceed();
38        }
39    }
40
41    internal class AddLoggerAttribute : TypeAspect
42    {
43        [Introduce]
44        private TextWriter _logger = File.CreateText( "log.txt" );
45    }
46}
1namespace Doc.ValidateAfterAllAspects
2{
3    [AddLogger]
4    internal class OkClass
5    {
6        [Log]
7        private void Bar() { }
8    }
9
    Error MY001: The type ErrorClass must have a field named _logger.

10    internal class ErrorClass
11    {
12        [Log]
13        private void Bar() { }
14    }
15}

Validating code references

Aspects can validate not only the declaration to which they are applied but also how this target declaration is used. In other words, aspects can validate code references.

To create an aspect that validates references:

  1. In the aspect class, define one or more static fields of type DiagnosticDefinition as explained in Reporting and suppressing diagnostics.
  2. Create a method of arbitrary name with the signature void ValidateReference( ReferenceValidationContext context ). Implement the validation logic in this method. All the data you need is in the ReferenceValidationContext object. When you detect a rule violation, report a diagnostic as described in Reporting and suppressing diagnostics. Alternatively, you can create a class implementing the ReferenceValidator abstract class.
  3. Override or implement the BuildAspect method of your aspect. From this method:
    1. Access the builder.Outbound property,
    2. Select declarations to be validated using the SelectMany and Select methods,
    3. Call the ValidateReferences method and pass a delegate to the validation method or an instance of the validator class.

Example: ForTestOnly, aspect implementation

The following example implements a custom attribute [ForTestOnly] that enforces that the target of this attribute can only be used from a namespace that ends with .Tests..

1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using Metalama.Framework.Validation;
5using System;
6
7namespace Doc.ForTestOnly
8{
9    [AttributeUsage(
10        AttributeTargets.Class | AttributeTargets.Struct |
11        AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property |
12        AttributeTargets.Event )]
13    public class ForTestOnlyAttribute : Attribute, IAspect<IMember>
14    {
15        private static DiagnosticDefinition<IDeclaration> _warning = new(
16            "MY001",
17            Severity.Warning,
18            "'{0}' can only be invoked from a namespace that ends with Tests." );
19
20        public void BuildAspect( IAspectBuilder<IMember> builder )
21        {
22            builder.Outbound.ValidateReferences( this.ValidateReference, ReferenceKinds.All );
23        }
24
25        private void ValidateReference( in ReferenceValidationContext context )
26        {
27            if (
28                context.ReferencingType != context.ReferencedDeclaration.GetClosestNamedType() &&
29                !context.ReferencingType.Namespace.FullName.EndsWith( ".Tests", StringComparison.Ordinal ) )
30            {
31                context.Diagnostics.Report( _warning.WithArguments( context.ReferencedDeclaration ) );
32            }
33        }
34    }
35}
1using System;
2
3namespace Doc.ForTestOnly
4{
5    public class MyService
6    {
7        // Normal constructor.
8        public MyService() : this( DateTime.Now ) { }
9
10        [ForTestOnly]
11        internal MyService( DateTime dateTime ) { }
12    }
13
14    internal class NormalClass
15    {
16        // Usage NOT allowed here because we are not in a Tests namespace.
        Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.

17        private MyService _service = new( DateTime.Now.AddDays( 1 ) );
18    }
19
20    namespace Tests
21    {
22        internal class TestClass
23        {
24            // Usage allowed here because we are in a Tests namespace.
25            private MyService _service = new( DateTime.Now.AddDays( 2 ) );
26        }
27    }
28}