File: ParserTests.LogProperties.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.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Gen.Logging.Parsing;
using Microsoft.Gen.Shared;
using VerifyXunit;
using Xunit;
 
namespace Microsoft.Gen.Logging.Test;
 
public partial class ParserTests
{
    [Fact]
    public static async Task InvalidLogPropertiesUsage()
    {
        await RunGenerator(@"
            class MyClass2
            {
                public int A { get; set; }
            }   
 
            class MyClass
            {
                [/*0+*/LogProperties/*-0*/]
                internal MyClass2 P0 { get; set; }
 
                [/*1+*/LogProperties/*-1*/]
                internal static MyClass2 P1 { get; set; }
 
                [/*2+*/LogProperties/*-2*/]
                internal MyClass2 P2 { set; }
 
                [/*3+*/LogProperties/*-3*/]
                public MyClass2 P3 { internal get; set; }
 
                public int A { get; set; }
            }
 
            partial class C
            {
                [LoggerMessage(0, LogLevel.Debug, ""Parameter"")]
                static partial void M0(ILogger logger, [LogProperties] MyClass p1);
            }", DiagDescriptors.InvalidAttributeUsage);
    }
 
    [Fact]
    public static async Task LogPropertiesOmitParamName_DetectsNameCollision()
    {
        const string Source = @"
            class MyType
            {
                public int p0 { get; }
            }
 
            partial class C
            {
                [LoggerMessage(LogLevel.Debug)]
                static partial void M0(ILogger logger, int p0, [LogProperties(OmitReferenceName = true)] MyType /*0+*/p1/*-0*/);
            }";
 
        await RunGenerator(Source, DiagDescriptors.TagNameCollision);
    }
 
    [Fact]
    public static async Task LogProperties_AllowsDefaultLogMethodCtor()
    {
        await RunGenerator(@"
            class MyType
            {
                public int p0 { get; }
            }
 
            partial class C
            {
                [LoggerMessage]
                static partial void M0(ILogger logger, LogLevel level, [LogProperties] MyType p0);
            }");
    }
 
    [Theory]
    [InlineData("record class")]
    [InlineData("record struct")]
    [InlineData("readonly record struct")]
    public async Task LogProperties_AllowsRecordTypes(string type)
    {
        await RunGenerator(@$"
            internal {type} MyRecord(int Value)
            {{
                public int GetOnlyValue => Value + 1;
            }}
 
            partial class C
            {{
                [LoggerMessage(LogLevel.Debug)]
                public static partial void LogFunc(ILogger logger, [LogProperties] MyRecord p0);
            }}");
    }
 
    [Theory]
    [InlineData("LogLevel")]
    [InlineData("System.Exception")]
    public async Task LogPropertiesInvalidUsage(string annotation)
    {
        // We don't check [LogProperties] on ILogger here since it produces a lot of errors
        string source = @$"
            partial class C
            {{
                [LoggerMessage(0, LogLevel.Debug, ""Parameterless..."")]
                static partial void M(ILogger logger, [LogProperties] {annotation} /*0+*/param/*-0*/);
            }}";
 
        await RunGenerator(source, DiagDescriptors.LogPropertiesInvalidUsage);
    }
 
    [Theory]
    [InlineData("MyClass")]
    [InlineData("MyStruct")]
    [InlineData("MyInterface")]
    public async Task LogPropertiesValidUsage(string parameterType)
    {
        await RunGenerator(@$"
            class MyClass
            {{
                public string Property {{ get; set; }}
            }}
 
            struct MyStruct
            {{
                public string Property {{ get; set; }}
            }}
 
            struct MyInterface
            {{
                public string Property {{ get; set; }}
            }}
 
            partial class C
            {{
                [LoggerMessage(0, LogLevel.Debug, ""Parameter {{P1}}"")]
                static partial void M(ILogger logger, [LogProperties] {parameterType} p1);
            }}");
    }
 
    [Theory]
    [InlineData("MyClass")]
    [InlineData("MyStruct")]
    [InlineData("MyInterface")]
    public async Task LogPropertiesParameterSkipped(string parameterType)
    {
        string source = @$"
            class MyClass {{ }}
 
            struct MyStruct {{ }}
 
            struct MyInterface {{ }}
 
            partial class C
            {{
                [LoggerMessage(0, LogLevel.Debug, ""Empty template"")]
                static partial void M(ILogger logger, [LogProperties] {parameterType} /*0+*/param/*-0*/);
            }}";
 
        await RunGenerator(source, DiagDescriptors.LogPropertiesParameterSkipped);
    }
 
    [Fact]
    public async Task LogPropertiesParameterNotSkipped()
    {
        await RunGenerator(@"
            class MyClass
            {
                public int A { get; set; }
            }
 
            partial class C
            {
                [LoggerMessage(0, LogLevel.Debug, ""{Param} is here"")]
                static partial void M(ILogger logger, [LogProperties] MyClass param);
            }");
    }
 
    [Fact]
    public async Task LogPropertiesPointlessUsage()
    {
        const string Source = @"
            class MyClass
            {
                private int A { get; set; }
                protected double B { get; set; }
            }
 
            partial class C
            {
                [LoggerMessage(0, LogLevel.Debug, ""{param}"")]
                static partial void M(ILogger logger, [LogProperties] MyClass /*0+*/param/*-0*/);
            }";
 
        await RunGenerator(Source, DiagDescriptors.LogPropertiesParameterSkipped);
    }
 
    [Fact]
    public async Task SimpleNameCollision()
    {
        const string Source = @"
            partial class C
            {
                [LoggerMessage(0, LogLevel.Debug, ""{Param} {Param}"")]
                static partial void M(ILogger logger, string param, string /*0+*/Param/*-0*/);
            }";
 
        await RunGenerator(Source, DiagDescriptors.TagNameCollision);
    }
 
    [Fact]
    public async Task LogPropertiesNameCollision()
    {
        const string Source = @"
            class MyClass
            {
                public int A { get; set; }
 
                [LogPropertyIgnore]
                public int B { get; set; }
            }
 
            partial class C
            {
                [LoggerMessage(LogLevel.Debug)]
                static partial void M0(ILogger logger, string param_A, [LogProperties] MyClass /*0+*/param/*-0*/);
 
                [LoggerMessage(LogLevel.Debug)]
                static partial void M1(ILogger logger, string param_B, [LogProperties] MyClass param);
            }";
 
        await RunGenerator(Source, DiagDescriptors.TagNameCollision);
    }
 
    [Fact]
    public async Task LogPropertiesTransitiveNameCollision()
    {
        const string Source = @"
            public class MyClass
            {
                public int Transitive_Prop { get; set; }
 
                [LogProperties]
                public MyTransitiveClass Transitive { get; set; }
            }
 
            public class MyTransitiveClass
            {
                public int Prop { get; set; }
            }
 
            partial class C
            {
                [LoggerMessage(0, LogLevel.Debug, ""No params..."")]
                static partial void M(ILogger logger, [LogProperties] MyClass /*0+*/param/*-0*/);
            }";
 
        await RunGenerator(Source, DiagDescriptors.TagNameCollision);
    }
 
    [Theory]
    [InlineData("int")]
    [InlineData("int?")]
    [InlineData("System.Int32")]
    [InlineData("System.Int32?")]
    [InlineData("bool")]
    [InlineData("bool?")]
    [InlineData("System.Boolean")]
    [InlineData("System.Boolean?")]
    [InlineData("byte")]
    [InlineData("byte?")]
    [InlineData("char?")]
    [InlineData("string")]
    [InlineData("string?")]
    [InlineData("double?")]
    [InlineData("decimal?")]
    [InlineData("object")]
    [InlineData("object?")]
    [InlineData("System.Object")]
    [InlineData("System.Object?")]
    [InlineData("int[]")]
    [InlineData("int?[]")]
    [InlineData("int[]?")]
    [InlineData("int?[]?")]
    [InlineData("object[]")]
    [InlineData("object[]?")]
    [InlineData("System.Array")]
    [InlineData("System.DateTime")]
    [InlineData("System.DateTimeOffset")]
    [InlineData("System.TimeSpan")]
    [InlineData("System.Guid")]
    [InlineData("System.DateTime?")]
    [InlineData("System.DateTimeOffset?")]
    [InlineData("System.TimeSpan?")]
    [InlineData("System.Guid?")]
    [InlineData("System.IDisposable")]
    [InlineData("System.Action")]
    [InlineData("System.Action<int>")]
    [InlineData("System.Func<double>")]
    [InlineData("System.Nullable<int>")]
    [InlineData("System.Nullable<char>")]
    [InlineData("System.Nullable<System.Int32>")]
    [InlineData("System.Nullable<System.Decimal>")]
    [InlineData("System.Nullable<System.DateTime>")]
    [InlineData("System.Nullable<System.DateTimeOffset>")]
    [InlineData("System.Nullable<System.TimeSpan>")]
    [InlineData("System.Nullable<System.Guid>")]
    public async Task IneligibleTypeForPropertiesLogging(string type)
    {
        string source = @$"
            class MyClass
            {{
                [LogProperties]
                public {type} /*0+*/Prop/*-0*/ {{ get; set; }}
            }}
 
            partial class C
            {{
                [LoggerMessage(0, LogLevel.Debug, ""No params..."")]
                static partial void M0(ILogger logger, [LogProperties] {type} /*1+*/test/*-1*/);
 
                [LoggerMessage(1, LogLevel.Debug, ""No params..."")]
                static partial void M1(ILogger logger, [LogProperties] MyClass test);
            }}";
 
        await RunGenerator(source, DiagDescriptors.InvalidTypeToLogProperties);
    }
 
    [Theory]
    [InlineData("ClassA")] // Type self-reference
    [InlineData("ClassC")] // One-level cycle
    [InlineData("ClassB")] // Two-level cycle
    [InlineData("StructA")] // Custom struct
    [InlineData("StructA?")] // Nullable struct
    [InlineData("System.Nullable<StructA>")] // Explicit nullable struct
    public async Task PropertyToLogWithTypeCycle(string propertyType)
    {
        string source = @$"
            public class ClassA
            {{
                [LogProperties]
                public {propertyType} Prop {{ get; set; }}
            }}
 
            public class ClassB
            {{
                [LogProperties]
                public ClassC Prop {{ get; set; }}
            }}
 
            public class ClassC
            {{
                [LogProperties]
                public ClassA Prop {{ get; set; }}
            }}
 
            public struct StructA
            {{
                [LogProperties]
                public ClassA Prop {{ get; set; }}
            }}
 
            partial class LoggerClass
            {{
                [LoggerMessage(0, LogLevel.Debug, ""No params..."")]
                static partial void M(ILogger logger, [LogProperties] ClassA /*0+*/test/*-0*/);
            }}";
 
        await RunGenerator(source, DiagDescriptors.LogPropertiesCycleDetected);
    }
 
    [Fact]
    public async Task PropertyHiddenInDerivedClass()
    {
        const string Source = @"
            public class BaseType
            {
                public int Prop { get; set; }
            }
 
            public class DerivedType : BaseType
            {
                public new string Prop { get; set; }
            }
 
            partial class LoggerClass
            {
                [LoggerMessage(0, LogLevel.Debug, ""No params..."")]
                static partial void M(ILogger logger, [LogProperties] DerivedType /*0+*/test/*-0*/);
            }";
 
        await RunGenerator(Source, DiagDescriptors.LogPropertiesHiddenPropertyDetected);
    }
 
    [Fact]
    public async Task DefaultToString()
    {
        await RunGenerator(@"
            record class MyRecordClass(int x);
            record struct MyRecordStruct(int x);
 
            class MyClass2
            {
            }
 
            class MyClass3
            {
                public override string ToString() => ""FIND ME!"";
            }
 
            class MyClass<T>
            {
                public object /*0+*/P0/*-0*/ { get; set; }
                public MyClass2 /*1+*/P1/*-1*/ { get; set; }
                public MyClass3 P2 { get; set; }
                public int P3 { get; set; }
                public System.Numerics.BigInteger P4 { get; set; }
                public T P5 { get; set; }
            }
 
            partial class C<T>
            {
                [LoggerMessage(LogLevel.Debug)]
                static partial void M0(this ILogger logger,
                    object /*2+*/p0/*-2*/,
                    MyClass2 /*3+*/p1/*-3*/,
                    MyClass3 p2,
                    [LogProperties] MyClass<int> p3,
                    T p4,
                    MyRecordClass p5,
                    MyRecordStruct p6);
            }", DiagDescriptors.DefaultToString);
    }
 
    [Fact]
    public async Task ClassWithNullableProperty()
    {
        string source = @"
                namespace Test
                {
                    using System;
 
                    using Microsoft.Extensions.Logging;
 
                    internal static class LoggerUtils
                    {
                        public class MyClassWithNullableProperty
                        {
                            public DateTime? NullableDateTime { get; set; }
                            public DateTime NonNullableDateTime { get; set; }
                        }
 
                        partial class MyLogger
                        {
                            [LoggerMessage(0, LogLevel.Information, ""Testing nullable property within class here..."")]
                            public static partial void LogMethodNullablePropertyInClassMatchesNonNullable(ILogger logger, [LogProperties] MyClassWithNullableProperty classWithNullablePropertyParam);
                        }
                    }
                }";
 
#if NET6_0_OR_GREATER
        var symbols = new[] { "NET7_0_OR_GREATER", "NET6_0_OR_GREATER", "NET5_0_OR_GREATER" };
#else
        var symbols = new[] { "NET5_0_OR_GREATER" };
#endif
 
        var (d, r) = await RoslynTestUtils.RunGenerator(
            new LoggingGenerator(),
            new[]
            {
                Assembly.GetAssembly(typeof(ILogger))!,
                Assembly.GetAssembly(typeof(LogPropertiesAttribute))!,
                Assembly.GetAssembly(typeof(LoggerMessageAttribute))!,
                Assembly.GetAssembly(typeof(DateTime))!,
            },
            [source],
            symbols);
 
        Assert.Empty(d);
        await Verifier.Verify(r[0].SourceText.ToString())
            .AddScrubber(_ => _.Replace(GeneratorUtilities.CurrentVersion, "VERSION"))
            .UseDirectory(Path.Combine("..", "Verified"));
    }
}