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:
- Define one or more static fields of type DiagnosticDefinition as explained in Reporting and suppressing diagnostics.
- 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. - Override or implement the BuildAspect method of your aspect. From this method:
- Access the builder.Outbound property,
- Call the AfterAllAspects() or BeforeAnyAspect() method to select the version of the code model,
- Select declarations to be validated using the SelectMany and Select methods,
- 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().
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.
In order to optimize performance, Metalama tries to avoid validating every single code reference. Instead, it has a concept of validator granularity (ReferenceGranularity), which accepts the values Compilation
, Namespace
, Type
, Member
, or ParameterOrAttribute
. When a validator is invariant within some level of granularity, then its predicate should only be evaluated once within the declaration at this level of granularity. For instance, if a validator granularity is set to Namespace
, then all references within that namespace will be either valid or invalid at the same time. Therefore, Metalama will only make a single call for this whole namespace. The validator method will be prevented from accessing details of a finer granularity level than the declared one. Warnings will be reported on all code references.
To create an aspect that validates references:
- In the aspect class, define one or more static fields of type DiagnosticDefinition as explained in Reporting and suppressing diagnostics.
- 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 InboundReferenceValidator abstract class. - Override or implement the BuildAspect method of your aspect. From this method:
- Access the builder.Outbound property,
- Select declarations to be validated using the SelectMany and Select methods,
- Call the ValidateInboundReferences method. Pass a delegate to the validation method or an instance of the validator class and the ReferenceGranularity.
Note
The delegate passed to the ValidateInboundReferences method must point to a named method of the aspect.
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.
.