Metalama 1.0 / / Metalama Documentation / Conceptual Documentation / Creating Aspects / Reporting and Suppressing Diagnostics

Reporting and Suppressing Diagnostics

This article explains how to report a diagnostic (error, warning or information message) from an aspect, or to suppress a diagnostic reported by the C# compiler or another aspect.

Benefits

  • Avoid non-intuitive error messages. Aspects that are applied to unexpected or untested kinds of declarations can throw very confusing exceptions or cause errors while compiling the transformed code. This confusion can be avoided by reporting clear error messages when the target of the aspect does not meet expectations. See also Defining the Eligibility of Aspects for this use case.
  • Avoid confusing warnings. The C# compiler and other analyzers are not aware that the code is being transformed by your aspect and they may, therefore, report irrelevant warnings. If you suppress these warnings from your aspects, developers using your aspect will be less confused and will not lose time suppressing the warnings manually.
  • Improve the productivity of the users of your aspect. Overall, reporting and suppressing relevant diagnostics greatly improves the productivity of people using your aspect.
  • Diagnostic-only aspects. You can also create aspects that only report or suppress diagnostics, without transforming source code. See Validating Code for details and benefits.

Reporting a diagnostic

To report a diagnostic:

  1. Import the Metalama.Framework.Diagnostics namespace.

  2. Define a static field 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 type int and string.

      Warning

      The aspect framework relies on the fact that diagnostics are defined as static fields of aspect classes. You will not be able to report a diagnostic that has not been declared on an aspect class of the current project.

  3. To report a diagnostic, use the builder.Diagnostics.Report method.

    The first parameter of the Report method is optional: it specifies the declaration to which the diagnostic relates. The aspect framework computes the file, line and column of the diagnostic based on this declaration. If you don't give a value for this parameter, the diagnostic will be reported for the target declaration of the aspect.

Example

The following aspect needs a field named _logger to exist in the target type. Its BuildAspect method checks that this field exists and reports an error if it does not. and in everything I've seen you have also introduced Fabrics that show how you can 'automate' logging methods in a project. I look at this example now and think to myself 'well this would never fire becuase I'd have a fabric that applies the log attribute to everything anyway -->

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using System.Linq;

namespace Doc.ReportError
{
    internal class LogAttribute : OverrideMethodAspect
    {
        // You MUST have a static field that defines the diagnostic.
        private static DiagnosticDefinition<INamedType> _error = new(
            "MY001",
            Severity.Error,
            "The type {0} must have a field named '_logger'." );

        public override void BuildAspect( IAspectBuilder<IMethod> builder )
        {
            // Validation must be done in BuildAspect. In OverrideMethod, it's too late.
            if ( !builder.Target.DeclaringType.Fields.OfName( "_logger" ).Any() )
            {
                builder.Diagnostics.Report( _error.WithArguments( builder.Target.DeclaringType ) );
            }
        }

        public override dynamic? OverrideMethod()
        {
            meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );

            return meta.Proceed();
        }
    }
}
namespace Doc.ReportError
{
    internal class Program
    {
        // Intentionally omitting the _logger field so an error is reported.

        [Log]
        private void Foo() { }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}

Suppressing a diagnostic

Sometimes the C# compiler or other analyzers may report warnings to the target code of your aspects. Since neither the C# compiler nor the analyzers know about your aspect, some of these warnings may be irrelevant. As an aspect author, it is a good practice to prevent the report of irrelevant warnings.

To suppress a diagnostic:

  1. Import the Metalama.Framework.Diagnostics namespace.

  2. Define a static field of type SuppressionDefinition in your aspect class. SuppressionDefinition specifies the identifier of the diagnostic to suppress.

  3. Call the Suppress method using builder.Diagnostics.Suppress(...) in the BuildAspect method.

Example

The following logging aspect requires a _logger field to exist, but it is likely that this field will never be used in user code but only in generated code. Because the IDE does not see the generated code, it will report the CS0169 warning, which is misleading and annoying to the user. The aspect suppresses this warning.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using System.Linq;

namespace Doc.SuppressWarning
{
    internal class LogAttribute : OverrideMethodAspect
    {
        private static SuppressionDefinition _suppression = new( "CS0169" );

        public override void BuildAspect( IAspectBuilder<IMethod> builder )
        {
            base.BuildAspect( builder );

            var loggerField = builder.Target.DeclaringType.Fields.OfName( "_logger" ).FirstOrDefault();

            if ( loggerField != null )
            {
                // Suppress "Field is necer read" warning from Intellisense warning for this field.
                builder.Diagnostics.Suppress( _suppression, loggerField );
            }
        }

        public override dynamic? OverrideMethod()
        {
            meta.This._logger.WriteLine( $"Executing {meta.Target.Method}." );

            return meta.Proceed();
        }
    }
}
using System;
using System.IO;

namespace Doc.SuppressWarning
{
    internal class Program
    {
        private TextWriter _logger = Console.Out;

        [Log]
        private void Foo() { }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
using System;
using System.IO;

namespace Doc.SuppressWarning
{
    internal class Program
    {
#pragma warning disable CS0169
        private TextWriter _logger = Console.Out;
#pragma warning restore CS0169

        [Log]
        private void Foo()
        {
            this._logger.WriteLine($"Executing Program.Foo().");
            return;
        }

        private static void Main()
        {
            new Program().Foo();
        }
    }
}
Executing Program.Foo().

Advanced Example

The following aspect can be added to a field or property. It overrides the getter so that its value is retrieved from a service locator. This aspect assumes that the target class has a field named _serviceProvider and of type IServiceProvider. The aspect reports errors if this field is absent or of a wrong type. The C# compiler may report an error CS0169 because it looks from source code that the _serviceProvider field is unused. Therefore, the aspect must suppress this diagnostic.

using Metalama.Framework.Aspects;
using Metalama.Framework.Code;
using Metalama.Framework.Diagnostics;
using System;
using System.Linq;

namespace Doc.ImportService
{
    internal class ImportAspect : OverrideFieldOrPropertyAspect
    {
        private static readonly DiagnosticDefinition<INamedType> _serviceProviderFieldMissing = new(
            "MY001",
            Severity.Error,
            "The 'ImportServiceAspect' aspects requires the type '{0}' to have a field named '_serviceProvider' and " +
            " of type 'IServiceProvider'." );

        private static readonly DiagnosticDefinition<(IField, IType)> _serviceProviderFieldTypeMismatch = new(
            "MY002",
            Severity.Error,
            "The type of field '{0}' must be 'IServiceProvider', but it is '{1}." );

        private static readonly SuppressionDefinition _suppressFieldIsNeverUsed = new( "CS0169" );

        public override void BuildAspect( IAspectBuilder<IFieldOrProperty> builder )
        {
            // Get the field _serviceProvider and check its type.
            var serviceProviderField =
                builder.Target.DeclaringType.Fields.OfName( "_serviceProvider" ).SingleOrDefault();

            if ( serviceProviderField == null )
            {
                builder.Diagnostics.Report( _serviceProviderFieldMissing.WithArguments( builder.Target.DeclaringType ) );

                return;
            }
            else if ( !serviceProviderField.Type.Is( typeof(IServiceProvider) ) )
            {
                builder.Diagnostics.Report(
                    _serviceProviderFieldTypeMismatch.WithArguments(
                        (serviceProviderField,
                         serviceProviderField.Type) ) );

                return;
            }

            // Provide the advice.
            base.BuildAspect( builder );

            // Suppress the diagnostic.
            builder.Diagnostics.Suppress( _suppressFieldIsNeverUsed, serviceProviderField );
        }

        public override dynamic? OverrideProperty
        {
            get => meta.This._serviceProvider.GetService( meta.Target.FieldOrProperty.Type.ToType() );

            set => throw new NotSupportedException();
        }
    }
}
using System;

namespace Doc.ImportService
{
    internal class Foo
    {
        // readonly IServiceProvider _serviceProvider;

        [ImportAspect]
        private IFormatProvider? FormatProvider { get; }

        public string Format( object? o )
        {
            return ((ICustomFormatter) this.FormatProvider!.GetFormat( typeof(ICustomFormatter) )!)
                .Format( null, o, this.FormatProvider );
        }
    }
}

Validating the target code after all aspects have been applied

When the BuildAspect method of your aspect is executed, it sees 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.