Validating references
It is often useful to validate how a declaration is used outside its parent type or namespace. With Metalama, you can analyze code references and:
- report errors and warnings for the code reference;
- suppress warnings reported by the C# compiler or by another analyzer for the code reference;
- suggest a code fix for the code reference.
You can validate references from an aspect or a fabric; approaches are very similar.
Validating references from an aspect
Additionally, both, or instead of transforming the code of the target declaration, an aspect can validate how the target declaration is being used, i.e., it can validate references to its target.
To create an aspect that validates references:
- Create an aspect class by inheriting one of the following classes, according to the kind of declarations you want to validate: CompilationAspect, ConstructorAspect, EventAspect, FieldAspect, FieldOrPropertyAspect, MethodAspect, ParameterAspect, PropertyAspect, TypeAspect or TypeParameterAspect. If you want to validate several kinds of declarations, you can inherit your class from Attribute and implement as many generic constructions of the IAspect<T> interface as needed.
- 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. - Override the AmendType, AmendNamespace or AmendProject method of your fabric.
- From the
Amend*
method, access the amender.Outbound property, select the declarations to be validated using the SelectMany and Select methods, then chain with a call to ValidateReferences. Pass two parameters:- The name of the new method you defined in the previous step. It must be a method, not a delegate, lambda, or a local function.
- The kinds of references you want to validate, for instance,
All
to validate all references,TypeOf
to validate references in atypeof
expression, and so on.
Example: ForTestOnly, aspect implementation
The following example implements a custom attribute [ForTestOnly]
that enforces that the target of this attribute can be used only from a namespace that ends with .Tests.
.
using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using Metalama.Framework.Validation;
using System;
namespace Doc.ForTestOnly
{
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Struct |
AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property |
AttributeTargets.Event )]
public class ForTestOnlyAttribute : Attribute, IAspect<IMember>
{
private static DiagnosticDefinition<IDeclaration> _warning = new(
"MY001",
Severity.Warning,
"'{0}' can only be invoked from a namespace that ends with Tests." );
public void BuildAspect( IAspectBuilder<IMember> builder )
{
builder.Outbound.ValidateReferences( this.ValidateReference, ReferenceKinds.All );
}
private void ValidateReference( in ReferenceValidationContext context )
{
if (
context.ReferencingType != context.ReferencedDeclaration.GetClosestNamedType() &&
!context.ReferencingType.Namespace.FullName.EndsWith( ".Tests", StringComparison.Ordinal ) )
{
context.Diagnostics.Report( _warning.WithArguments( context.ReferencedDeclaration ) );
}
}
}
}
using System;
namespace Doc.ForTestOnly
{
public class MyService
{
// Normal constructor.
public MyService() : this( DateTime.Now ) { }
[ForTestOnly]
internal MyService( DateTime dateTime ) { }
}
internal class NormalClass
{
// 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.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
Warning MY001: 'MyService.MyService(DateTime)' can only be invoked from a namespace that ends with Tests.
private MyService _service = new( DateTime.Now.AddDays( 1 ) );
}
namespace Tests
{
internal class TestClass
{
// Usage allowed here because we are in a Tests namespace.
private MyService _service = new( DateTime.Now.AddDays( 2 ) );
}
}
}
using System;
namespace Doc.ForTestOnly
{
public class MyService
{
// Normal constructor.
public MyService() : this(DateTime.Now) { }
[ForTestOnly]
internal MyService(DateTime dateTime) { }
}
internal class NormalClass
{
// Usage NOT allowed here because we are not in a Tests namespace.
private MyService _service = new(DateTime.Now.AddDays(1));
}
namespace Tests
{
internal class TestClass
{
// Usage allowed here because we are in a Tests namespace.
private MyService _service = new(DateTime.Now.AddDays(2));
}
}
}
Validating references from a fabric
The steps to validate code from a fabric are almost the same as from an aspect, but the difference is that you do not create an aspect class but a fabric class.
- Create a fabric class by inheriting one of the following classes according to the kind of declarations you want to validate:
- TypeFabric (the fabric class must be a nested class) to validate the containing class;
- NamespaceFabric to validate the containing namespace;
- ProjectFabric to validate the current project;
- TransitiveProjectFabric to validate any project referencing the current project.
- In the fabric 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( in 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. - Override the AmendType, AmendNamespace or AmendProject method of your fabric.
- From the
Amend*
method, call builder.With, then chain with a call to ValidateReferences. Pass two parameters:- The new method that you have defined in the previous step. It must be a method. It cannot be a delegate, lambda, or a local function.
- The kinds of references you want to validate, for instance,
All
to validate all references,TypeOf
to validate references in atypeof
expression, and so on. You can select multiple kinds of references using the|
operator.
Example: ForTestOnly, fabric implementation
The following example implements the same logic as the previous one but uses a fabric instead of an aspect.
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using Metalama.Framework.Fabrics;
using Metalama.Framework.Validation;
using System;
namespace Doc.ForTestOnly_Fabric
{
namespace ValidatedNamespace
{
public class Fabric : NamespaceFabric
{
private static DiagnosticDefinition<IDeclaration> _warning = new(
"MY001",
Severity.Warning,
"'{0}' can only be invoked from a namespace that ends with '.Tests'." );
public override void AmendNamespace( INamespaceAmender amender )
{
amender.Outbound.ValidateReferences( this.ValidateReference, ReferenceKinds.All );
}
private void ValidateReference( in ReferenceValidationContext context )
{
if (
context.ReferencingType.Namespace != context.ReferencedDeclaration &&
!context.ReferencingType.Namespace.FullName.EndsWith( ".Tests", StringComparison.Ordinal ) )
{
context.Diagnostics.Report( _warning.WithArguments( context.ReferencedDeclaration ) );
}
}
}
}
}
using Doc.ForTestOnly_Fabric.ValidatedNamespace;
namespace Doc.ForTestOnly_Fabric
{
namespace ValidatedNamespace
{
public class MyService { }
}
internal class NormalClass
{
// Usage NOT allowed here.
Warning MY001: 'Doc.ForTestOnly_Fabric.ValidatedNamespace' can only be invoked from a namespace that ends with '.Tests'.
Warning MY001: 'Doc.ForTestOnly_Fabric.ValidatedNamespace' can only be invoked from a namespace that ends with '.Tests'.
Warning MY001: 'Doc.ForTestOnly_Fabric.ValidatedNamespace' can only be invoked from a namespace that ends with '.Tests'.
private MyService _service = new();
}
namespace Tests
{
internal class TestClass
{
// Usage allowed here because we are in the Tests namespace.
private MyService _service = new();
}
}
}
// Warning MY001 on `new()`: `'Doc.ForTestOnly_Fabric.ValidatedNamespace' can only be invoked from a namespace that ends with '.Tests'.`
using Doc.ForTestOnly_Fabric.ValidatedNamespace;
namespace Doc.ForTestOnly_Fabric
{
namespace ValidatedNamespace
{
public class MyService { }
}
internal class NormalClass
{
// Usage NOT allowed here.
private MyService _service = new();
}
namespace Tests
{
internal class TestClass
{
// Usage allowed here because we are in the Tests namespace.
private MyService _service = new();
}
}
}
Cross-project validation
When a validator is added to a non-private member, the scope is not limited to the current project. Code references in referenced projects will also be validated.
To implement this feature, the aspects are serialized during the build and stored as an embedded resource in each binary assembly. This resource is then deserialized when the assembly is referenced in another project.
The cross-project scenario and the need for serialization are why the validation code must be in a stand-alone aspect or fabric class method.
Passing state to the validation method
You must use builder.AspectState. Do not store in an aspect field state that depends on the target of the aspect.