File: ParserTests.cs
Web Access
Project: src\test\Generators\Microsoft.Gen.Logging\Unit\Microsoft.Gen.Logging.Unit.Tests.csproj (Microsoft.Gen.Logging.Unit.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Numerics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Testing;
using Microsoft.Extensions.Diagnostics.Enrichment;
using Microsoft.Extensions.Logging;
using Microsoft.Gen.Logging.Parsing;
using Microsoft.Gen.Shared;
using VerifyXunit;
using Xunit;
 
namespace Microsoft.Gen.Logging.Test;
 
[UsesVerify]
public partial class ParserTests
{
    private const int TotalSensitiveCases = 21;
 
    [Fact]
    public async Task IncompatibleAttributes()
    {
        await RunGenerator(@$"
            namespace TestClasses
            {{
                public partial class LoggerInPropertyTestClass
                {{
                    public ILogger Logger {{ get; set; }} = null!;
 
                    [LoggerMessage(0, LogLevel.Debug, ""M0 {{p0}}"")]
                    public partial void M0(string p0);
                }}
 
                public partial class LoggerInNullablePropertyTestClass
                {{
                    public ILogger? Logger {{ get; set; }}
 
                    [LoggerMessage(0, LogLevel.Debug, ""M0 {{p0}}"")]
                    public partial void M0(string p0);
                }}
 
                public partial class GenericLoggerInPropertyTestClass
                {{
                    public ILogger<int> Logger {{ get; set; }} = null!;
 
                    [LoggerMessage(0, LogLevel.Debug, ""M0 {{p0}}"")]
                    public partial void M0(string p0);
                }}
 
                public partial class LoggerInPropertyDerivedTestClass : LoggerInPropertyTestClass
                {{
                    [LoggerMessage(1, LogLevel.Debug, ""M1 {{p0}}"")]
                    public partial void M1(string p0);
                }}
 
                public partial class LoggerInNullablePropertyDerivedTestClass : LoggerInNullablePropertyTestClass
                {{
                    [LoggerMessage(1, LogLevel.Debug, ""M1 {{p0}}"")]
                    public partial void M1(string p0);
                }}
 
                public partial class GenericLoggerInPropertyDerivedTestClass : LoggerInNullablePropertyTestClass
                {{
                    [LoggerMessage(1, LogLevel.Debug, ""M1 {{p0}}"")]
                    public partial void M1(string p0);
                }}
            }}
        ");
    }
 
    [Fact]
    public async Task NullableStructEnumerable()
    {
        await RunGenerator(@"
            using System.Collections.Generic;
 
            namespace TestClasses
            {
                public readonly struct StructEnumerable : IEnumerable<int>
                {
                    private static readonly List<int> _numbers = new() { 1, 2, 3 };
                    public IEnumerator<int> GetEnumerator() => _numbers.GetEnumerator();
                    IEnumerator IEnumerable.GetEnumerator() => _numbers.GetEnumerator();
                }
 
                internal static partial class NullableTestExtensions
                {
                    /// <summary>
                    /// A comment!
                    /// </summary>
                    [LoggerMessage(13, LogLevel.Error, ""M13{p1}"")]
                    public static partial void M13(ILogger logger, StructEnumerable p1);
 
                    [LoggerMessage(14, LogLevel.Error, ""M14{p1}"")]
                    public static partial void M14(ILogger logger, StructEnumerable? p1);
                }
            }");
    }
 
    [Fact]
    public async Task MissingAttributeValue()
    {
        await RunGenerator(@"
                internal static partial class C
                {
                    [LoggerMessage(0, LogLevel.Debug, ""M0 {p0}"", EventName = )]
                    static partial void M0(ILogger logger, string p0);
 
                    [LoggerMessage(1, LogLevel.Debug, ""M0 {p0}"", SkipEnabledChecks = )]
                    static partial void M1(ILogger logger, string p0);
                }
            ");
    }
 
    [Fact]
    public async Task WithNullLevel_GeneratorWontFail()
    {
        await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage(0, null, ""This is a message with {foo}"")]
                    static partial void M1(ILogger logger, string foo);
                }
            ");
    }
 
    [Fact]
    public async Task WithNullEventId_GeneratorWontFail()
    {
        await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage(null, LogLevel.Debug, ""This is a message with {foo}"")]
                    static partial void M1(ILogger logger, string foo);
                }
            ");
    }
 
    [Fact]
    public async Task WithNullMessage_GeneratorWontFail()
    {
        await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage(0, LogLevel.Debug, null)]
                    static partial void M1(ILogger logger, string foo);
                }
            ");
    }
 
    [Fact]
    public async Task ParameterlessConstructor()
    {
        await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage()]
                    static partial void M1(ILogger logger, LogLevel level, string foo);
 
                    [LoggerMessage]
                    static partial void M2(ILogger logger, LogLevel level, string foo);
 
                    [LoggerMessage(SkipEnabledChecks = true)]
                    static partial void M3(ILogger logger, LogLevel level, string foo);
                }");
    }
 
    [Fact]
    public async Task UnderscoresInMethodName()
    {
        const string Source = @"
                partial class C
                {
                    [LoggerMessage(0, LogLevel.Debug, ""M1"")]
                    static partial void __M1(ILogger logger);
                }
            ";
 
        await RunGenerator(Source);
    }
 
    [Fact]
    public async Task MissingLogLevel()
    {
        const string Source = @"
                partial class C
                {
                    /*0+*/[LoggerMessage(""M1"")]
                    static partial void M1(ILogger logger);/*-0*/
               }
            ";
 
        await RunGenerator(Source, DiagDescriptors.MissingLogLevel);
    }
 
    [Fact]
    public async Task MissingLogLevel_WhenDefaultCtor()
    {
        const string Source = @"
                partial class C
                {
                    /*0+*/[LoggerMessage]
                    static partial void M1(ILogger logger);/*-0*/
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.MissingLogLevel);
    }
 
    [Fact]
    public async Task MissingTemplate()
    {
        const string Source = @"
                partial class C
                {
                    [LoggerMessage(0, LogLevel.Debug, ""This is a message without foo"")]
                    static partial void M1(ILogger logger, string /*0+*/foo/*-0*/);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.ParameterHasNoCorrespondingTemplate);
    }
 
    [Fact]
    public async Task MissingArgument()
    {
        const string Source = @"
                partial class C
                {
                    [/*0+*/LoggerMessage(0, LogLevel.Debug, ""{foo}"")/*-0*/]
                    static partial void M1(ILogger logger);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.TemplateHasNoCorrespondingParameter);
    }
 
    [Theory]
    [InlineData("{@param}")]
    [InlineData("{@param,10}")]
    [InlineData("{@param,10:5}")]
    [InlineData("{@param:5}")]
    [InlineData("{@param }")]
    [InlineData("{ @param}")]
    [InlineData("{ @param }")]
    [InlineData("{ @param:10 }")]
    [InlineData("{ @param : 10 }")]
    [InlineData("{ @param, 10 }")]
    [InlineData("{ @param , 10 }")]
    [InlineData("{ @param , 10 : 5 }")]
    [InlineData(" Beginning ... {  @param  } ending ")]
    [InlineData(" Beginning ... {  @param , 10 : 5 } ending ")]
    public async Task AtSymbolInTemplate(string template)
    {
        string source = @$"
            partial class C
            {{
                [/*0+*/LoggerMessage(LogLevel.Debug, ""{template}"")/*-0*/]
                static partial void M1(ILogger logger, int param);
            }}";
 
        await RunGenerator(source);
    }
 
    [Theory]
    [InlineData("{request}", "request")]
    [InlineData("{request}", "@request")]
    [InlineData("{@request}", "request")]
    [InlineData("{@request}", "@request")]
    public async Task AtSymbolArgumentOutOfOrder(string stringTemplate, string parameterName)
    {
        await RunGenerator(@$"
                partial class C
                {{
                    [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""{stringTemplate} {{a1}}"")]
                    static partial void M1(ILogger logger,string a1, string {parameterName});
                }}
            ");
    }
 
    [Fact]
    public async Task NeedlessQualifierInMessage()
    {
        const string Source = @"
                partial class C
                {
                    [/*0+*/LoggerMessage(0, LogLevel.Information, ""INFO: this is an informative message"")/*-0*/]
                    static partial void M1(ILogger logger);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.RedundantQualifierInMessage);
    }
 
    [Fact]
    public async Task NeedlessExceptionInMessage()
    {
        const string Source = @"
                partial class C
                {
                    [/*0+*/LoggerMessage(0, LogLevel.Debug, ""M1 {ex} {ex2}"")/*-0*/]
                    static partial void M1(ILogger logger, System.Exception ex, System.Exception ex2);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.ShouldntMentionExceptionInMessage);
    }
 
    [Fact]
    public async Task NeedlessLogLevelInMessage()
    {
        const string Source = @"
                partial class C
                {
                    [/*0+*/LoggerMessage(0, ""M1 {l1} {l2}"")/*-0*/]
                    static partial void M1(ILogger logger, LogLevel l1, LogLevel l2);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.ShouldntMentionLogLevelInMessage);
    }
 
    [Fact]
    public async Task NeedlessLoggerInMessage()
    {
        const string Source = @"
                partial class C
                {
                    [/*0+*/LoggerMessage(0, LogLevel.Debug, ""M1 {logger}"")/*-0*/]
                    static partial void M1(ILogger logger);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.ShouldntMentionLoggerInMessage);
    }
 
    [Fact]
    public async Task FileScopedNamespace()
    {
        await RunGenerator(@"
            namespace Test;
            partial class C
            {
                [LoggerMessage(0, LogLevel.Debug, ""{P1}"")]
                static partial void M1(ILogger logger, int p1);
            }", inNamespace: false);
    }
 
    [Theory]
    [InlineData("_foo")]
    [InlineData("__foo")]
    [InlineData("@_foo", "_foo")]
    public async Task UnderscoresInParameterName(string name, string? template = null)
    {
        string source = @$"
                partial class C
                {{
                    [LoggerMessage(0, LogLevel.Debug, ""M1 {{{template ?? name}}}"")]
                    static partial void M1(ILogger logger, string {name});
                }}
            ";
 
        await RunGenerator(source);
    }
 
    [Fact]
    public async Task MissingExceptionType()
    {
        const string Source = @"
                namespace System
                {
                    public class Object {}
                    public class Void {}
                    public class String {}
                    public struct DateTime {}
                    public class Attribute {}
                }
                namespace System.Collections
                {
                    public interface IEnumerable {}
                }
                namespace Microsoft.Extensions.Logging
                {
                    public enum LogLevel {}
                    public interface ILogger {}
                }
                namespace Microsoft.Extensions.Logging
                {
                    public class LoggerMessageAttribute : System.Attribute {}
                }
                partial class C
                {
                    [Microsoft.Extensions.Logging.LoggerMessageAttribute()]
                    public static partial void Something(this Microsoft.Extensions.Logging.ILogger logger);
                }";
 
        await RunGenerator(Source, DiagDescriptors.MissingRequiredType, false, includeBaseReferences: false, includeLoggingReferences: false);
    }
 
    [Fact]
    public async Task MissingEnrichmentPropertyBagTypes()
    {
        const string Source = @"
                namespace Microsoft.Extensions.Logging
                {
                    public enum LogLevel {}
                    public interface ILogger {}
                }
                namespace Microsoft.Extensions.Logging
                {
                    public class LoggerMessageAttribute : System.Attribute {}
                    public class LogPropertiesAttribute : System.Attribute {}
                    public class LogPropertyIgnoreAttribute : System.Attribute {}
                    public class ITagCollector {}
                    public class LogMethodHelper { }
                }
                partial class C
                {
                    [Microsoft.Extensions.Logging.LoggerMessageAttribute]
                    public static partial void Something(this Microsoft.Extensions.Logging.ILogger logger, Microsoft.Extensions.Logging.LogLevel level, string foo);
                }";
 
        await RunGenerator(Source, DiagDescriptors.MissingRequiredType, wrap: false, includeLoggingReferences: false);
    }
 
    [Fact]
    public async Task MissingLoggerMessageAttributeType()
    {
        await RunGenerator(@"
                partial class C
                {
                }
            ", includeLoggingReferences: false);
    }
 
    [Fact]
    public async Task MissingILoggerType()
    {
        await RunGenerator(@"
                namespace Microsoft.Extensions.Logging
                {
                    public sealed class LoggerMessageAttribute : System.Attribute {}
                }
                partial class C
                {
                }
            ", includeLoggingReferences: false);
    }
 
    [Fact]
    public async Task MissingLogLevelType()
    {
        await RunGenerator(@"
                namespace Microsoft.Extensions.Logging
                {
                    public sealed class LoggerMessageAttribute : System.Attribute {}
                }
                namespace Microsoft.Extensions.Logging
                {
                    public interface ILogger {}
                }
                partial class C
                {
                }
            ", includeLoggingReferences: false);
    }
 
    [Fact]
    public async Task EventIdReuse()
    {
        const string Source = @"
                partial class MyClass
                {
                    [LoggerMessage(0, LogLevel.Debug, ""M1"")]
                    static partial void M1(ILogger logger);
 
                    [/*0+*/LoggerMessage(0, LogLevel.Debug, ""M1"")/*-0*/]
                    static partial void M2(ILogger logger);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.ShouldntReuseEventIds);
    }
 
    [Fact]
    public async Task EventNameReuse()
    {
        const string Source = @"
                partial class MyClass
                {
                    [LoggerMessage(0, LogLevel.Debug, ""M1"", EventName = ""Dog"")]
                    static partial void M1(ILogger logger);
 
                    [/*0+*/LoggerMessage(1, LogLevel.Debug, ""M1"", EventName = ""Dog"")/*-0*/]
                    static partial void M2(ILogger logger);
                }
            ";
 
        await RunGenerator(Source, DiagDescriptors.ShouldntReuseEventNames);
    }
 
    [Fact]
    public async Task ValidTemplates()
    {
        await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = """")]
                    static partial void M1(ILogger logger);
 
                    [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = ""M2"")]
                    static partial void M2(ILogger logger);
 
                    [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = ""{arg1}"")]
                    static partial void M3(ILogger logger, int arg1);
 
                    [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = ""M4 {arg1}"")]
                    static partial void M4(ILogger logger, int arg1);
 
                    [LoggerMessage(EventId = 5, Level = LogLevel.Debug, Message = ""{arg1} M5"")]
                    static partial void M5(ILogger logger, int arg1);
 
                    [LoggerMessage(EventId = 6, Level = LogLevel.Debug, Message = ""M6{arg1}M6{arg2}M6"")]
                    static partial void M6(ILogger logger, string arg1, string arg2);
 
                    [LoggerMessage(EventId = 7, Level = LogLevel.Debug, Message = ""M7 {{const}}"")]
                    static partial void M7(ILogger logger);
 
                    [LoggerMessage(EventId = 8, Level = LogLevel.Debug, Message = ""{{prefix{{{arg1}}}suffix}}"")]
                    static partial void M8(ILogger logger, string arg1);
 
                    [LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = ""prefix }}"")]
                    static partial void M9(ILogger logger);
 
                    [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = ""}}suffix"")]
                    static partial void M10(ILogger logger);
                }
            ");
    }
 
    [Fact]
    public async Task MalformedFormatString()
    {
        await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""M1 {A} M1 { M1"")]
                    static partial void /*0+*/M1/*-0*/(ILogger logger);
 
                    [LoggerMessage(EventId = 2, Level = LogLevel.Debug, Message = ""M2 {A} M2 } M2"")]
                    static partial void /*1+*/M2/*-1*/(ILogger logger);
 
                    [LoggerMessage(EventId = 3, Level = LogLevel.Debug, Message = ""M3 {arg1"")]
                    static partial void /*2+*/M3/*-2*/(ILogger logger);
 
                    [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = ""M4 arg1}"")]
                    static partial void /*3+*/M4/*-3*/(ILogger logger);
 
                    [LoggerMessage(EventId = 5, Level = LogLevel.Debug, Message = ""M5 {"")]
                    static partial void /*4+*/M5/*-4*/(ILogger logger);
 
                    [LoggerMessage(EventId = 6, Level = LogLevel.Debug, Message = ""}M6 "")]
                    static partial void /*5+*/M6/*-5*/(ILogger logger);
 
                    [LoggerMessage(EventId = 7, Level = LogLevel.Debug, Message = ""{M7{"")]
                    static partial void /*6+*/M7/*-6*/(ILogger logger);
 
                    [LoggerMessage(EventId = 8, Level = LogLevel.Debug, Message = ""{{{arg1 M8"")]
                    static partial void /*7+*/M8/*-7*/(ILogger logger);
 
                    [LoggerMessage(EventId = 9, Level = LogLevel.Debug, Message = ""arg1}}} M9"")]
                    static partial void /*8+*/M9/*-8*/(ILogger logger);
 
                    [LoggerMessage(EventId = 10, Level = LogLevel.Debug, Message = ""{} M10"")]
                    static partial void /*9+*/M10/*-9*/(ILogger logger);
 
                    [LoggerMessage(EventId = 11, Level = LogLevel.Debug, Message = ""{ } M11"")]
                    static partial void /*10+*/M11/*-10*/(ILogger logger);
                }
            ", DiagDescriptors.MalformedFormatStrings);
    }
 
    [Fact]
    public async Task Cancellation()
    {
        await Assert.ThrowsAsync<OperationCanceledException>(async () =>
            await RunGenerator(@"
                partial class C
                {
                    [LoggerMessage(0, LogLevel.Debug, ""M1"")]
                    static partial void M1(ILogger logger);
                }
            ", cancellationToken: new CancellationToken(true)));
    }
 
    [Fact]
    public async Task SourceErrors()
    {
        await RunGenerator(@"
                static partial class C
                {
                    // bogus argument type
                    [LoggerMessage(0, "", ""Hello"")]
                    static partial void M1(ILogger logger);
 
                    // missing parameter name
                    [LoggerMessage(1, LogLevel.Debug, ""Hello"")]
                    static partial void M2(ILogger);
 
                    // bogus parameter type
                    [LoggerMessage(2, LogLevel.Debug, ""Hello"")]
                    static partial void M3(XILogger logger);
 
                    // bogus enum value
                    [LoggerMessage(3, LogLevel.Foo, ""Hello"")]
                    static partial void M4(ILogger logger);
 
                    // attribute applied to something other than a method
                    [LoggerMessage(4, "", ""Hello"")]
                    int M5;
                }
            ");
    }
 
    [Fact]
    public Task MultipleTypeDefinitions()
    {
        // Adding a dependency to an assembly that has internal definitions of public types
        // should not result in a collision and break generation.
        // Verify usage of the extension GetBestTypeByMetadataName(this Compilation)
        // instead of Compilation.GetTypeByMetadataName().
        var referencedSource = @"
                namespace Microsoft.Extensions.Logging
                {
                    internal class LoggerMessageAttribute { }
                }
                namespace Microsoft.Extensions.Logging
                {
                    internal interface ILogger { }
                    internal enum LogLevel { }
                }";
 
        // Compile the referenced assembly first.
        Compilation referencedCompilation = CompilationHelper.CreateCompilation(referencedSource);
 
        // Obtain the image of the referenced assembly.
        byte[] referencedImage = CompilationHelper.CreateAssemblyImage(referencedCompilation);
 
        // Generate the code
        string source = @"
                namespace Test
                {
                    using Microsoft.Extensions.Logging;
 
                    partial class C
                    {
                        [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""M1"")]
                        static partial void M1(ILogger logger);
                    }
                }";
 
        MetadataReference[] additionalReferences = { MetadataReference.CreateFromImage(referencedImage) };
 
        Compilation compilation = CompilationHelper.CreateCompilation(source, additionalReferences);
        LoggingGenerator generator = new LoggingGenerator();
 
        (IReadOnlyList<Diagnostic> diagnostics, ImmutableArray<GeneratedSourceResult> generatedSources) =
            RoslynTestUtils.RunGenerator(compilation, generator);
 
        // Make sure compilation was successful.
        Assert.Empty(diagnostics);
        Assert.Single(generatedSources);
 
        return Verifier.Verify(generatedSources[0].SourceText.ToString())
            .AddScrubber(_ => _.Replace(GeneratorUtilities.CurrentVersion, "VERSION"))
            .UseDirectory(Path.Combine("..", "Verified"));
    }
 
#pragma warning disable S107 // Methods should not have too many parameters
    private static async Task RunGenerator(
        string code,
        DiagnosticDescriptor? expectedDiagnostic = null,
        bool wrap = true,
        bool inNamespace = true,
        bool includeBaseReferences = true,
        bool includeLoggingReferences = true,
        DiagnosticDescriptor? ignoreDiag = null,
        CancellationToken cancellationToken = default)
#pragma warning restore S107 // Methods should not have too many parameters
    {
        var text = code;
        if (wrap)
        {
            var nspaceStart = "namespace Test {";
            var nspaceEnd = "}";
            if (!inNamespace)
            {
                nspaceStart = "";
                nspaceEnd = "";
            }
 
            text = $@"
                    {nspaceStart}
                    using Microsoft.Extensions.Logging;
                    {code}
                    {nspaceEnd}";
        }
 
        Assembly[]? refs = null;
        if (includeLoggingReferences)
        {
            refs = new[]
            {
                Assembly.GetAssembly(typeof(ILogger))!,
                Assembly.GetAssembly(typeof(LoggerMessageAttribute))!,
                Assembly.GetAssembly(typeof(IEnrichmentTagCollector))!,
                Assembly.GetAssembly(typeof(DataClassification))!,
                Assembly.GetAssembly(typeof(PrivateDataAttribute))!,
                Assembly.GetAssembly(typeof(BigInteger))!,
            };
        }
 
        var (d, sources) = await RoslynTestUtils.RunGenerator(
            new LoggingGenerator(),
            refs,
            new[] { text },
            includeBaseReferences: includeBaseReferences,
            cancellationToken: cancellationToken).ConfigureAwait(false);
 
        if (ignoreDiag != null)
        {
            d = d.FilterOutDiagnostics(ignoreDiag);
        }
 
        if (expectedDiagnostic != null)
        {
            RoslynTestUtils.AssertDiagnostics(text, expectedDiagnostic, d);
        }
        else if (d.Count > 0)
        {
            Assert.Fail($"Expected no diagnostics, got {d.Count} diagnostics");
        }
    }
}