This article provides guidance on how to report a diagnostic (including errors, warnings, or information messages) from an aspect, or suppress a diagnostic reported by the C# compiler or another aspect.
Benefits
- Prevent non-intuitive compilation errors: Aspects applied to unexpected or untested kinds of declarations can lead to confusing exceptions or errors during compilation of the transformed code. Mitigate this confusion by reporting clear error messages when the target of the aspect fails to meet expectations. Refer to Defining the eligibility of aspects for this use case.
- Eliminate confusing warnings: The C# compiler and other analyzers, unaware of the code transformation by your aspect, may report irrelevant warnings. Suppressing these warnings with your aspect can reduce confusion and save developers from manually suppressing them.
- Enhance user productivity: Overall, reporting and suppressing relevant diagnostics can significantly improve the productivity of those using your aspect.
- Diagnostic-only aspects: You can create aspects that solely report or suppress diagnostics without transforming any source code. Refer to Validating code from an aspect for additional details and benefits.
Reporting a diagnostic
To report a diagnostic:
Import the Metalama.Framework.Diagnostics namespace.
Define a
staticfield of type DiagnosticDefinition in your aspect class. DiagnosticDefinition specifies the diagnostic ID, the severity, and the message formatting string.- For a message without formatting parameters or with weakly-typed formatting parameters, use the non-generic DiagnosticDefinition class.
- For a message with a single strongly-typed formatting parameter, use the generic DiagnosticDefinition<T> class, e.g.,
DiagnosticDefinition<int>. - For a message with several strongly-typed formatting parameters, use the generic DiagnosticDefinition<T> with a tuple, e.g.,
DiagnosticDefinition<(int,string)>for a message with two formatting parameters expecting a value of typeintandstring.
Warning
The aspect framework relies on diagnostics being defined as static fields of aspect classes. You won't be able to report a diagnostic that hasn't been declared on an aspect class of the current project.
To report a diagnostic, use the builder.Diagnostics.Report method.
The second parameter of the
Reportmethod is optional: it specifies the declaration to which the diagnostic relates. Based on this declaration, the aspect framework computes the diagnostic file, line, and column. If you don't provide a value for this parameter, the diagnostic will be reported for the target declaration of the aspect.
Example
The following aspect requires a field named _logger to exist in the target type. Its BuildAspect method checks the existence of this field and reports an error if it is absent.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using System.Linq;
5
6namespace Doc.ReportError;
7
8internal class LogAttribute : OverrideMethodAspect
9{
10 // You MUST have a static field that defines the diagnostic.
11 private static readonly DiagnosticDefinition<INamedType> _error = new(
12 "MY001",
13 Severity.Error,
14 "The type {0} must have a field named '_logger'." );
15
16 public override void BuildAspect( IAspectBuilder<IMethod> builder )
17 {
18 base.BuildAspect( builder );
19
20 // Validation must be done in BuildAspect. In OverrideMethod, it's too late.
21 if ( !builder.Target.DeclaringType.Fields.OfName( "_logger" ).Any() )
22 {
23 builder.Diagnostics.Report( _error.WithArguments( builder.Target.DeclaringType ) );
24 }
25 }
26
27 public override dynamic? OverrideMethod()
28 {
29 meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );
30
31 return meta.Proceed();
32 }
33}
1namespace Doc.ReportError;
2
3internal class Program
4{
5 // Intentionally omitting the _logger field so an error is reported.
6
7 [Log]
Error MY001: The type Program must have a field named '_logger'.
8 private void Foo() { }
9
10 private static void Main()
11 {
12 new Program().Foo();
13 }
14}
Suppressing a diagnostic
The C# compiler or other analyzers may report warnings to the target code of your aspects. Since neither the C# compiler nor the analyzers are aware of your aspect, some of these warnings may be irrelevant. As an aspect author, prevent the reporting of irrelevant warnings.
To suppress a diagnostic:
Import the Metalama.Framework.Diagnostics namespace.
Define a
staticfield of type SuppressionDefinition in your aspect class. SuppressionDefinition specifies the identifier of the diagnostic to suppress.Call the Suppress method using
builder.Diagnostics.Suppress(...)in theBuildAspectmethod and supply the SuppressionDefinition created above. The suppression will apply to the current target of the aspect unless you specify a different scope as an argument.
These steps will suppress all warnings of the specified ID in the scope of the current target of the aspect.
Example
The following logging aspect requires a _logger field. This field will be used in generated code but never in user code. Because the IDE doesn't see the generated code, it will report the CS0169 warning, which is misleading and annoying to the user. The aspect suppresses this warning.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using System.Linq;
5
6namespace Doc.SuppressWarning;
7
8internal class LogAttribute : OverrideMethodAspect
9{
10 private static readonly SuppressionDefinition _suppression = new( "CS0169" );
11
12 public override void BuildAspect( IAspectBuilder<IMethod> builder )
13 {
14 base.BuildAspect( builder );
15
16 var loggerField = builder.Target.DeclaringType.Fields.OfName( "_logger" ).FirstOrDefault();
17
18 if ( loggerField != null )
19 {
20 // Suppress "Field is never read" warning from Intellisense warning for this field.
21 builder.Diagnostics.Suppress( _suppression, loggerField );
22 }
23 }
24
25 public override dynamic? OverrideMethod()
26 {
27 meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );
28
29 return meta.Proceed();
30 }
31}
1using System;
2using System.IO;
3
4namespace Doc.SuppressWarning;
5
6internal class Program
7{
8 private TextWriter _logger = Console.Out;
9
10 [Log]
11 private void Foo() { }
12
13 private static void Main()
14 {
15 new Program().Foo();
16 }
17}
1using System;
2using System.IO;
3
4namespace Doc.SuppressWarning;
5
6internal class Program
7{
8 private TextWriter _logger = Console.Out;
9
10 [Log]
11 private void Foo()
12 {
13 _logger.WriteLine("Executing Program.Foo().");
14 }
15
16 private static void Main()
17 {
18 new Program().Foo();
19 }
20}
Executing Program.Foo().
Filtering suppressions by diagnostic arguments
To selectively suppress only specific diagnostics matching certain criteria, use the WithFilter method. The filter receives an ISuppressibleDiagnostic that provides access to:
- Id: the diagnostic ID (e.g.,
CS0219). - InvariantMessage: the formatted message in English. Note that message text may change between Roslyn versions.
- Arguments: the raw arguments passed to the message formatter, often including symbol names.
- Span: the source location.
This is useful when you want to suppress warnings for specific symbols without suppressing all warnings of the same ID in the scope.
Example: filtering by variable name
The following aspect suppresses the CS0219 warning ("variable is assigned but never used") only for variables named _initialized. Other variables with the same warning in the same scope will still report the warning.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using System;
5using System.Linq;
6
7namespace Doc.SuppressWarningWithFilter;
8
9internal class SuppressUnusedVariableAttribute : OverrideMethodAspect
10{
11 private static readonly SuppressionDefinition _suppressUnusedVariable = new( "CS0219" );
12
13 public override void BuildAspect( IAspectBuilder<IMethod> builder )
14 {
15 base.BuildAspect( builder );
16
17 // Suppress "variable is assigned but never used" only for variables named "_initialized".
18 // Other CS0219 warnings in the same scope will still be reported.
19 builder.Diagnostics.Suppress(
20 _suppressUnusedVariable.WithFilter(
21 d => d.Arguments.Any( a => a is string s && s == "_initialized" ) ),
22 builder.Target );
23 }
24
25 public override dynamic? OverrideMethod()
26 {
27 Console.WriteLine( $"Entering {meta.Target.Method.Name}" );
28
29 return meta.Proceed();
30 }
31}
32
1namespace Doc.SuppressWarningWithFilter;
2
3
4internal class MyService
5{
6 [SuppressUnusedVariable]
7 public void Initialize()
8 {
9 // The CS0219 warning for this variable is suppressed by the aspect
10 // because the variable is named "_initialized".
11 var _initialized = true;
12
13 // The CS0219 warning for this variable is NOT suppressed
14 // because the filter only matches variables named "_initialized".
Warning CS0219: The variable '_other' is assigned but its value is never used
15 var _other = 42;
16 }
17}
18
1using System;
2
3namespace Doc.SuppressWarningWithFilter;
4
5
6internal class MyService
7{
8 [SuppressUnusedVariable]
9 public void Initialize()
10 {
11 Console.WriteLine("Entering Initialize");
12 // The CS0219 warning for this variable is suppressed by the aspect
13 // because the variable is named "_initialized".
14 var _initialized = true;
15
16 // The CS0219 warning for this variable is NOT suppressed
17 // because the filter only matches variables named "_initialized".
Warning CS0219: The variable '_other' is assigned but its value is never used
18 var _other = 42;
19 }
20}
21
Advanced example
The following aspect can be added to a field or property. It overrides the getter implementation to retrieve the value from the service locator. This aspect assumes that the target class has a field named _serviceProvider of type IServiceProvider. The aspect reports errors if this field is absent or doesn't match the expected type. The C# compiler may report a warning CS0169 because it appears from the source code that the _serviceProvider field is unused. Therefore, the aspect must suppress this diagnostic.
1using Metalama.Framework.Aspects;
2using Metalama.Framework.Code;
3using Metalama.Framework.Diagnostics;
4using System;
5using System.Linq;
6
7namespace Doc.ImportService;
8
9internal class ImportAspect : OverrideFieldOrPropertyAspect
10{
11 private static readonly DiagnosticDefinition<INamedType> _serviceProviderFieldMissing = new(
12 "MY001",
13 Severity.Error,
14 "The 'ImportServiceAspect' aspects requires the type '{0}' to have a field named '_serviceProvider' and "
15 +
16 " of type 'IServiceProvider'." );
17
18 private static readonly DiagnosticDefinition<(IField, IType)>
19 _serviceProviderFieldTypeMismatch = new(
20 "MY002",
21 Severity.Error,
22 "The type of field '{0}' must be 'IServiceProvider', but it is '{1}." );
23
24 private static readonly SuppressionDefinition _suppressFieldIsNeverUsed = new( "CS0169" );
25
26 public override void BuildAspect( IAspectBuilder<IFieldOrProperty> builder )
27 {
28 // Get the field _serviceProvider and check its type.
29 var serviceProviderField =
30 builder.Target.DeclaringType.Fields.OfName( "_serviceProvider" ).SingleOrDefault();
31
32 if ( serviceProviderField == null )
33 {
34 builder.Diagnostics.Report(
35 _serviceProviderFieldMissing.WithArguments( builder.Target.DeclaringType ) );
36
37 return;
38 }
39 else if ( !serviceProviderField.Type.IsConvertibleTo( typeof(IServiceProvider) ) )
40 {
41 builder.Diagnostics.Report(
42 _serviceProviderFieldTypeMismatch.WithArguments(
43 (serviceProviderField,
44 serviceProviderField.Type) ) );
45
46 return;
47 }
48
49 // Provide the advice.
50 base.BuildAspect( builder );
51
52 // Suppress the diagnostic.
53 builder.Diagnostics.Suppress( _suppressFieldIsNeverUsed, serviceProviderField );
54 }
55
56 public override dynamic? OverrideProperty
57 {
58 get => meta.This._serviceProvider.GetService( meta.Target.FieldOrProperty.Type.ToType() );
59
60 set => throw new NotSupportedException();
61 }
62}
1using System;
2
3namespace Doc.ImportService;
4
5internal class Foo
6{
7 // readonly IServiceProvider _serviceProvider;
8
9 [ImportAspect]
Error MY001: The 'ImportServiceAspect' aspects requires the type 'Foo' to have a field named '_serviceProvider' and of type 'IServiceProvider'.
10 private IFormatProvider? FormatProvider { get; }
11
12 public string Format( object? o )
13 {
14 return ((ICustomFormatter) this.FormatProvider!.GetFormat( typeof(ICustomFormatter) )!)
15 .Format( null, o, this.FormatProvider );
16 }
17}
Validating the target code after all aspects have been applied
When your aspect's BuildAspect method is executed, it views the code model as it was before the aspect was applied.
If you need to validate the code after all aspects have been applied, see Validating code from an aspect.