Add `SkipEnabledCheck` on `LoggerMessageAttribute` (#54305)
authorMaryam Ariyan <maryam.ariyan@microsoft.com>
Tue, 22 Jun 2021 21:25:36 +0000 (17:25 -0400)
committerGitHub <noreply@github.com>
Tue, 22 Jun 2021 21:25:36 +0000 (14:25 -0700)
* Add SkipEnabledCheck on LoggerMessageAttribute
* Make logging generator more robust with null input
* Adds LoggerMessageAttribute ctor overload
* properly identify misconfigured input

13 files changed:
src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LoggerMessageAttribute.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithDefaultValues.generated.txt [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratedCodeTests.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/EventNameTestExtensions.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/LevelTestExtensions.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/MessageTestExtensions.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/SkipEnabledCheckExtensions.cs [new file with mode: 0644]

index 2b0bc4e..908efaf 100644 (file)
@@ -381,34 +381,45 @@ namespace {lc.Namespace}
                 GenParameters(lm);
 
                 _builder.Append($@")
-        {nestedIndentation}{{
+        {nestedIndentation}{{");
+
+                string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : "    ";
+                if (!lm.SkipEnabledCheck)
+                {
+                    _builder.Append($@"
             {nestedIndentation}if ({logger}.IsEnabled({level}))
             {nestedIndentation}{{");
+                }
 
                 if (UseLoggerMessageDefine(lm))
                 {
                     _builder.Append($@"
-                {nestedIndentation}__{lm.Name}Callback({logger}, ");
+            {nestedIndentation}{enabledCheckIndentation}__{lm.Name}Callback({logger}, ");
 
                     GenCallbackArguments(lm);
 
-                    _builder.Append(@$"{exceptionArg});");
+                    _builder.Append($"{exceptionArg});");
                 }
                 else
                 {
                     _builder.Append($@"
-                {nestedIndentation}{logger}.Log(
-                    {level},
-                    new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}),
-                    ");
-                    GenHolder(lm);
-                    _builder.Append($@",
-                    {exceptionArg},
-                    __{lm.Name}Struct.Format);");
+            {nestedIndentation}{enabledCheckIndentation}{logger}.Log(
+                {nestedIndentation}{enabledCheckIndentation}{level},
+                {nestedIndentation}{enabledCheckIndentation}new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}),
+                {nestedIndentation}{enabledCheckIndentation}");
+                GenHolder(lm);
+                _builder.Append($@",
+                {nestedIndentation}{enabledCheckIndentation}{exceptionArg},
+                {nestedIndentation}{enabledCheckIndentation}__{lm.Name}Struct.Format);");
+                }
+
+                if (!lm.SkipEnabledCheck)
+                {
+                    _builder.Append($@"
+            {nestedIndentation}}}");
                 }
 
                 _builder.Append($@"
-            {nestedIndentation}}}
         {nestedIndentation}}}");
 
                 static string GetException(LoggerMethod lm)
index 534249c..4a99503 100644 (file)
@@ -9,6 +9,7 @@ using System.Threading;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
 using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
 
 namespace Microsoft.Extensions.Logging.Generators
 {
@@ -28,7 +29,7 @@ namespace Microsoft.Extensions.Logging.Generators
             }
 
             /// <summary>
-            /// Gets the set of logging classes or structs containing methods to output.
+            /// Gets the set of logging classes containing methods to output.
             /// </summary>
             public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSyntax> classes)
             {
@@ -83,7 +84,7 @@ namespace Microsoft.Extensions.Logging.Generators
                         bool multipleLoggerFields = false;
 
                         ids.Clear();
-                        foreach (var member in classDec.Members)
+                        foreach (MemberDeclarationSyntax member in classDec.Members)
                         {
                             var method = member as MethodDeclarationSyntax;
                             if (method == null)
@@ -93,6 +94,9 @@ namespace Microsoft.Extensions.Logging.Generators
                             }
 
                             sm ??= _compilation.GetSemanticModel(classDec.SyntaxTree);
+                            IMethodSymbol logMethodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken) as IMethodSymbol;
+                            Debug.Assert(logMethodSymbol != null, "log method is present.");
+                            (int eventId, int? level, string message, string? eventName, bool skipEnabledCheck) = (-1, null, string.Empty, null, false);
 
                             foreach (AttributeListSyntax mal in method.AttributeLists)
                             {
@@ -105,7 +109,78 @@ namespace Microsoft.Extensions.Logging.Generators
                                         continue;
                                     }
 
-                                    (int eventId, int? level, string message, string? eventName) = ExtractAttributeValues(ma.ArgumentList!, sm);
+                                    bool hasMisconfiguredInput = false;
+                                    ImmutableArray<AttributeData>? boundAttrbutes = logMethodSymbol?.GetAttributes();
+
+                                    if (boundAttrbutes == null)
+                                    {
+                                        continue;
+                                    }
+
+                                    foreach (AttributeData attributeData in boundAttrbutes)
+                                    {
+                                        // supports: [LoggerMessage(0, LogLevel.Warning, "custom message")]
+                                        // supports: [LoggerMessage(eventId: 0, level: LogLevel.Warning, message: "custom message")]
+                                        if (attributeData.ConstructorArguments.Any())
+                                        {
+                                            foreach (TypedConstant typedConstant in attributeData.ConstructorArguments)
+                                            {
+                                                if (typedConstant.Kind == TypedConstantKind.Error)
+                                                {
+                                                    hasMisconfiguredInput = true;
+                                                }
+                                            }
+
+                                            ImmutableArray<TypedConstant> items = attributeData.ConstructorArguments;
+                                            Debug.Assert(items.Length == 3);
+
+                                            eventId = items[0].IsNull ? -1 : (int)GetItem(items[0]);
+                                            level = items[1].IsNull ? null : (int?)GetItem(items[1]);
+                                            message = items[2].IsNull ? "" : (string)GetItem(items[2]);
+                                        }
+
+                                        // argument syntax takes parameters. e.g. EventId = 0
+                                        // supports: e.g. [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = "custom message")]
+                                        if (attributeData.NamedArguments.Any())
+                                        {
+                                            foreach (KeyValuePair<string, TypedConstant> namedArgument in attributeData.NamedArguments)
+                                            {
+                                                TypedConstant typedConstant = namedArgument.Value;
+                                                if (typedConstant.Kind == TypedConstantKind.Error)
+                                                {
+                                                    hasMisconfiguredInput = true;
+                                                }
+                                                else
+                                                {
+                                                    TypedConstant value = namedArgument.Value;
+                                                    switch (namedArgument.Key)
+                                                    {
+                                                        case "EventId":
+                                                            eventId = (int)GetItem(value);
+                                                            break;
+                                                        case "Level":
+                                                            level = value.IsNull ? null : (int?)GetItem(value);
+                                                            break;
+                                                        case "SkipEnabledCheck":
+                                                            skipEnabledCheck = (bool)GetItem(value);
+                                                            break;
+                                                        case "EventName":
+                                                            eventName = (string?)GetItem(value);
+                                                            break;
+                                                        case "Message":
+                                                            message = value.IsNull ? "" : (string)GetItem(value);
+                                                            break;
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+
+                                    if (hasMisconfiguredInput)
+                                    {
+                                        // skip further generator execution and let compiler generate the errors
+                                        break;
+                                    }
 
                                     IMethodSymbol? methodSymbol = sm.GetDeclaredSymbol(method, _cancellationToken);
                                     if (methodSymbol != null)
@@ -119,6 +194,7 @@ namespace Microsoft.Extensions.Logging.Generators
                                             EventName = eventName,
                                             IsExtensionMethod = methodSymbol.IsExtensionMethod,
                                             Modifiers = method.Modifiers.ToString(),
+                                            SkipEnabledCheck = skipEnabledCheck
                                         };
 
                                         ExtractTemplates(message, lm.TemplateMap, lm.TemplateList);
@@ -435,35 +511,6 @@ namespace Microsoft.Extensions.Logging.Generators
                 return (loggerField, false);
             }
 
-            private (int eventId, int? level, string message, string? eventName) ExtractAttributeValues(AttributeArgumentListSyntax args, SemanticModel sm)
-            {
-                int eventId = 0;
-                int? level = null;
-                string? eventName = null;
-                string message = string.Empty;
-                foreach (AttributeArgumentSyntax a in args.Arguments)
-                {
-                    // argument syntax takes parameters. e.g. EventId = 0
-                    Debug.Assert(a.NameEquals != null);
-                    switch (a.NameEquals.Name.ToString())
-                    {
-                        case "EventId":
-                            eventId = (int)sm.GetConstantValue(a.Expression, _cancellationToken).Value!;
-                            break;
-                        case "EventName":
-                            eventName = sm.GetConstantValue(a.Expression, _cancellationToken).ToString();
-                            break;
-                        case "Level":
-                            level = (int)sm.GetConstantValue(a.Expression, _cancellationToken).Value!;
-                            break;
-                        case "Message":
-                            message = sm.GetConstantValue(a.Expression, _cancellationToken).ToString();
-                            break;
-                    }
-                }
-                return (eventId, level, message, eventName);
-            }
-
             private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs)
             {
                 _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs));
@@ -580,6 +627,8 @@ namespace Microsoft.Extensions.Logging.Generators
 
                 return string.Empty;
             }
+
+            private static object GetItem(TypedConstant arg) => arg.Kind == TypedConstantKind.Array ? arg.Values : arg.Value;
         }
 
         /// <summary>
@@ -612,6 +661,7 @@ namespace Microsoft.Extensions.Logging.Generators
             public bool IsExtensionMethod;
             public string Modifiers = string.Empty;
             public string LoggerField = string.Empty;
+            public bool SkipEnabledCheck;
         }
 
         /// <summary>
index e7b9034..ef9d465 100644 (file)
@@ -119,10 +119,12 @@ namespace Microsoft.Extensions.Logging
     public sealed partial class LoggerMessageAttribute : System.Attribute
     {
         public LoggerMessageAttribute() { }
+        public LoggerMessageAttribute(int eventId, Microsoft.Extensions.Logging.LogLevel level, string message) { }
         public int EventId { get { throw null; } set { } }
         public string? EventName { get { throw null; } set { } }
         public Microsoft.Extensions.Logging.LogLevel Level { get { throw null; } set { } }
         public string Message { get { throw null; } set { } }
+        public bool SkipEnabledCheck { get { throw null; } set { } }
     }
     public partial class Logger<T> : Microsoft.Extensions.Logging.ILogger, Microsoft.Extensions.Logging.ILogger<T>
     {
index b103ef3..acb9af3 100644 (file)
@@ -38,6 +38,20 @@ namespace Microsoft.Extensions.Logging
         public LoggerMessageAttribute() { }
 
         /// <summary>
+        /// Initializes a new instance of the <see cref="LoggerMessageAttribute"/> class
+        /// which is used to guide the production of a strongly-typed logging method.
+        /// </summary>
+        /// <param name="eventId">The log event Id.</param>
+        /// <param name="level">The log level.</param>
+        /// <param name="message">Format string of the log message.</param>
+        public LoggerMessageAttribute(int eventId, LogLevel level, string message)
+        {
+            EventId = eventId;
+            Level = level;
+            Message = message;
+        }
+
+        /// <summary>
         /// Gets the logging event id for the logging method.
         /// </summary>
         public int EventId { get; set; } = -1;
@@ -59,5 +73,10 @@ namespace Microsoft.Extensions.Logging
         /// Gets the message text for the logging method.
         /// </summary>
         public string Message { get; set; } = "";
+
+        /// <summary>
+        /// Gets the flag to skip IsEnabled check for the logging method.
+        /// </summary>
+        public bool SkipEnabledCheck { get; set; }
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithDefaultValues.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithDefaultValues.generated.txt
new file mode 100644 (file)
index 0000000..c61da37
--- /dev/null
@@ -0,0 +1,57 @@
+// <auto-generated/>
+#nullable enable
+
+namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
+{
+    partial class TestWithDefaultValues 
+    {
+        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")]
+        private readonly struct __M0Struct : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>
+        {
+
+            public override string ToString()
+            {
+
+                return $"";
+            }
+
+            public static string Format(__M0Struct state, global::System.Exception? ex) => state.ToString();
+
+            public int Count => 1;
+
+            public global::System.Collections.Generic.KeyValuePair<string, object?> this[int index]
+            {
+                get => index switch
+                {
+                    0 => new global::System.Collections.Generic.KeyValuePair<string, object?>("{OriginalFormat}", ""),
+
+                    _ => throw new global::System.IndexOutOfRangeException(nameof(index)),  // return the same exception LoggerMessage.Define returns in this case
+                };
+            }
+
+            public global::System.Collections.Generic.IEnumerator<global::System.Collections.Generic.KeyValuePair<string, object?>> GetEnumerator()
+            {
+                for (int i = 0; i < 1; i++)
+                {
+                    yield return this[i];
+                }
+            }
+
+            global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
+        }
+
+        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")]
+        public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, global::Microsoft.Extensions.Logging.LogLevel level)
+        {
+            if (logger.IsEnabled(level))
+            {
+                logger.Log(
+                    level,
+                    new global::Microsoft.Extensions.Logging.EventId(-1, nameof(M0)),
+                    new __M0Struct(),
+                    null,
+                    __M0Struct.Format);
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithSkipEnabledCheck.generated.txt
new file mode 100644 (file)
index 0000000..c4b242a
--- /dev/null
@@ -0,0 +1,18 @@
+// <auto-generated/>
+#nullable enable
+
+namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
+{
+    partial class TestWithSkipEnabledCheck 
+    {
+        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")]
+        private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, global::System.Exception?> __M0Callback =
+            global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", true); 
+
+        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "6.0.0.0")]
+        public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger)
+        {
+            __M0Callback(logger, null);
+        }
+    }
+}
\ No newline at end of file
index c9180b7..33d1ab7 100644 (file)
@@ -178,6 +178,22 @@ namespace Microsoft.Extensions.Logging.Generators.Tests
             Assert.Equal(string.Empty, logger.LastFormattedString);
             Assert.Equal(LogLevel.Debug, logger.LastLogLevel);
             Assert.Equal(1, logger.CallCount);
+
+            logger.Reset();
+            MessageTestExtensions.M5(logger, LogLevel.Trace);
+            Assert.Null(logger.LastException);
+            Assert.Equal(string.Empty, logger.LastFormattedString);
+            Assert.Equal(LogLevel.Trace, logger.LastLogLevel);
+            Assert.Equal(-1, logger.LastEventId.Id);
+            Assert.Equal(1, logger.CallCount);
+
+            logger.Reset();
+            MessageTestExtensions.M6(logger, LogLevel.Trace);
+            Assert.Null(logger.LastException);
+            Assert.Equal(string.Empty, logger.LastFormattedString);
+            Assert.Equal(LogLevel.Trace, logger.LastLogLevel);
+            Assert.Equal(6, logger.LastEventId.Id);
+            Assert.Equal(1, logger.CallCount);
         }
 
         [Fact]
@@ -309,6 +325,14 @@ namespace Microsoft.Extensions.Logging.Generators.Tests
             Assert.Equal("M9", logger.LastFormattedString);
             Assert.Equal(LogLevel.Trace, logger.LastLogLevel);
             Assert.Equal(1, logger.CallCount);
+
+            logger.Reset();
+            LevelTestExtensions.M10vs11(logger);
+            Assert.Null(logger.LastException);
+            Assert.Equal("event ID 10 vs. 11", logger.LastFormattedString);
+            Assert.Equal(LogLevel.Warning, logger.LastLogLevel);
+            Assert.Equal(1, logger.CallCount);
+            Assert.Equal(11, logger.LastEventId.Id);
         }
 
         [Fact]
@@ -343,6 +367,40 @@ namespace Microsoft.Extensions.Logging.Generators.Tests
             Assert.Equal(LogLevel.Trace, logger.LastLogLevel);
             Assert.Equal(1, logger.CallCount);
             Assert.Equal("CustomEventName", logger.LastEventId.Name);
+
+            logger.Reset();
+            EventNameTestExtensions.CustomEventName(logger);
+            Assert.Null(logger.LastException);
+            Assert.Equal("CustomEventName", logger.LastFormattedString);
+            Assert.Equal(LogLevel.Trace, logger.LastLogLevel);
+            Assert.Equal(1, logger.CallCount);
+            Assert.Equal("CustomEventName", logger.LastEventId.Name);
+        }
+
+        [Fact]
+        public void SkipEnabledCheckTests()
+        {
+            var logger = new MockLogger();
+
+            logger.Reset();
+            logger.Enabled = false;
+            Assert.False(logger.IsEnabled(LogLevel.Information));
+            SkipEnabledCheckExtensions.LoggerMethodWithFalseSkipEnabledCheck(logger);
+            Assert.Null(logger.LastException);
+            Assert.Null(logger.LastFormattedString);
+            Assert.Equal((LogLevel)(-1), logger.LastLogLevel);
+            Assert.Equal(0, logger.CallCount);
+            Assert.Equal(default, logger.LastEventId);
+
+            logger.Reset();
+            logger.Enabled = false;
+            Assert.False(logger.IsEnabled(LogLevel.Debug));
+            SkipEnabledCheckExtensions.LoggerMethodWithTrueSkipEnabledCheck(logger);
+            Assert.Null(logger.LastException);
+            Assert.Equal("Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", logger.LastFormattedString);
+            Assert.Equal(LogLevel.Debug, logger.LastLogLevel);
+            Assert.Equal(1, logger.CallCount);
+            Assert.Equal("LoggerMethodWithTrueSkipEnabledCheck", logger.LastEventId.Name);
         }
 
         [Fact]
index 433d601..917d87c 100644 (file)
@@ -35,17 +35,51 @@ namespace Microsoft.Extensions.Logging.Generators.Tests
         }
 
         [Fact]
-        public async Task TestBaseline_TestWithTwoParams_Success()
+        public async Task TestBaseline_TestWithSkipEnabledCheck_Success()
         {
             string testSourceCode = @"
 namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
 {
-    internal static partial class TestWithTwoParams
+    internal static partial class TestWithSkipEnabledCheck
     {
-        [LoggerMessage(EventId = 0, Level = LogLevel.Error, Message = ""M0 {a1} {a2}"")]
-        public static partial void M0(ILogger logger, int a1, System.Collections.Generic.IEnumerable<int> a2);
+        [LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = ""Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check."", SkipEnabledCheck = true)]
+        public static partial void M0(ILogger logger);
+    }
+}";
+            await VerifyAgainstBaselineUsingFile("TestWithSkipEnabledCheck.generated.txt", testSourceCode);
+        }
+
+        [Fact]
+        public async Task TestBaseline_TestWithDefaultValues_Success()
+        {
+            string testSourceCode = @"
+namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
+{
+    internal static partial class TestWithDefaultValues
+    {
+        [LoggerMessage]
+        public static partial void M0(ILogger logger, LogLevel level);
     }
 }";
+            await VerifyAgainstBaselineUsingFile("TestWithDefaultValues.generated.txt", testSourceCode);
+        }
+
+        [Theory]
+        [InlineData("EventId = 0, Level = LogLevel.Error, Message = \"M0 {a1} {a2}\"")]
+        [InlineData("eventId: 0, level: LogLevel.Error, message: \"M0 {a1} {a2}\"")]
+        [InlineData("0, LogLevel.Error, \"M0 {a1} {a2}\"")]
+        [InlineData("0, LogLevel.Error, \"M0 {a1} {a2}\", SkipEnabledCheck = false")]
+        public async Task TestBaseline_TestWithTwoParams_Success(string argumentList)
+        {
+            string testSourceCode = $@"
+namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
+{{
+    internal static partial class TestWithTwoParams
+    {{
+        [LoggerMessage({argumentList})]
+        public static partial void M0(ILogger logger, int a1, System.Collections.Generic.IEnumerable<int> a2);
+    }}
+}}";
             await VerifyAgainstBaselineUsingFile("TestWithTwoParams.generated.txt", testSourceCode);
         }
 
index 4991e41..8e5df49 100644 (file)
@@ -64,6 +64,90 @@ namespace Microsoft.Extensions.Logging.Generators.Tests
             Assert.Equal(DiagnosticDescriptors.LoggingMethodHasBody.Id, diagnostics[0].Id);
         }
 
+        [Theory]
+        [InlineData("EventId = 0, Level = null, Message = \"This is a message with {foo}\"")]
+        [InlineData("eventId: 0, level: null, message: \"This is a message with {foo}\"")]
+        [InlineData("0, null, \"This is a message with {foo}\"")]
+        public async Task WithNullLevel_GeneratorWontFail(string argumentList)
+        {
+            IReadOnlyList<Diagnostic> diagnostics = await RunGenerator($@"
+                partial class C
+                {{
+                    [LoggerMessage({argumentList})]
+                    static partial void M1(ILogger logger, string foo);
+                    
+                    [LoggerMessage({argumentList})]
+                    static partial void M2(ILogger logger, LogLevel level, string foo);
+                }}
+            ");
+
+            Assert.Empty(diagnostics);
+        }
+
+        [Theory]
+        [InlineData("EventId = null, Level = LogLevel.Debug, Message = \"This is a message with {foo}\"")]
+        [InlineData("eventId: null, level: LogLevel.Debug, message: \"This is a message with {foo}\"")]
+        [InlineData("null, LogLevel.Debug, \"This is a message with {foo}\"")]
+        public async Task WithNullEventId_GeneratorWontFail(string argumentList)
+        {
+            IReadOnlyList<Diagnostic> diagnostics = await RunGenerator($@"
+                partial class C
+                {{
+                    [LoggerMessage({argumentList})]
+                    static partial void M1(ILogger logger, string foo);
+                }}
+            ");
+
+            Assert.Empty(diagnostics);
+        }
+
+        [Fact]
+        public async Task WithNullMessage_GeneratorWontFail()
+        {
+            IReadOnlyList<Diagnostic> diagnostics = await RunGenerator(@"
+                partial class C
+                {
+                    [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = null)]
+                    static partial void M1(ILogger logger, string foo);
+                }
+            ");
+
+            Assert.Single(diagnostics);
+            Assert.Equal(DiagnosticDescriptors.ArgumentHasNoCorrespondingTemplate.Id, diagnostics[0].Id);
+            Assert.Contains("Argument 'foo' is not referenced from the logging message", diagnostics[0].GetMessage(), StringComparison.InvariantCulture);
+        }
+
+        [Fact]
+        public async Task WithNullSkipEnabledCheck_GeneratorWontFail()
+        {
+            IReadOnlyList<Diagnostic> diagnostics = await RunGenerator(@"
+                partial class C
+                {
+                    [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""This is a message with {foo}"", SkipEnabledCheck = null)]
+                    static partial void M1(ILogger logger, string foo);
+                }
+            ");
+
+            Assert.Empty(diagnostics);
+        }
+
+        [Fact]
+        public async Task WithBadMisconfiguredInput_GeneratorWontFail()
+        {
+            IReadOnlyList<Diagnostic> diagnostics = await RunGenerator(@"
+                public static partial class C
+                {
+                    [LoggerMessage(SkipEnabledCheck = 6)]
+                    public static partial void M0(ILogger logger, LogLevel level);
+
+                    [LoggerMessage(eventId: true, level: LogLevel.Debug, message: ""misconfigured eventId as bool"")]
+                    public static partial void M1(ILogger logger);
+                }
+            ");
+
+            Assert.Empty(diagnostics);
+        }
+
         [Fact]
         public async Task MissingTemplate()
         {
@@ -266,6 +350,26 @@ namespace Microsoft.Extensions.Logging.Generators.Tests
             Assert.Empty(diagnostics);
         }
 
+        [Theory]
+        [InlineData("false")]
+        [InlineData("true")]
+        [InlineData("null")]
+        public async Task UsingSkipEnabledCheck(string skipEnabledCheckValue)
+        {
+            IReadOnlyList<Diagnostic> diagnostics = await RunGenerator($@"
+                partial class C
+                {{
+                    public partial class WithLoggerMethodUsingSkipEnabledCheck
+                    {{
+                        [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""M1"", SkipEnabledCheck = {skipEnabledCheckValue})]
+                        static partial void M1(ILogger logger);
+                    }}
+                }}
+            ");
+
+            Assert.Empty(diagnostics);
+        }
+
         [Fact]
         public async Task MissingExceptionType()
         {
index 4c0ddf3..f41d615 100644 (file)
@@ -7,5 +7,8 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
     {
         [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = "M0", EventName = "CustomEventName")]
         public static partial void M0(ILogger logger);
+
+        [LoggerMessage(EventId = 2, Level = LogLevel.Trace, Message = "CustomEventName")] // EventName inferred from method name
+        public static partial void CustomEventName(ILogger logger);
     }
 }
index 5726aa0..2251d19 100644 (file)
@@ -34,5 +34,8 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
 
         [LoggerMessage(EventId = 9, Message = "M9")]
         public static partial void M9(LogLevel level, ILogger logger);
+
+        [LoggerMessage(eventId: 10, level: LogLevel.Warning, message: "event ID 10 vs. 11", EventId = 11)]
+        public static partial void M10vs11(ILogger logger);
     }
 }
index a308492..8cad8db 100644 (file)
@@ -29,5 +29,11 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
         [LoggerMessage(EventId = 4, Level = LogLevel.Debug, Message = "{p1}")]
         public static partial void M4(ILogger logger, string p1, int p2, int p3);
 #endif
+
+        [LoggerMessage]
+        public static partial void M5(ILogger logger, LogLevel level);
+
+        [LoggerMessage(EventId = 6, Message = "")]
+        public static partial void M6(ILogger logger, LogLevel level);
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/SkipEnabledCheckExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/TestClasses/SkipEnabledCheckExtensions.cs
new file mode 100644 (file)
index 0000000..397acdf
--- /dev/null
@@ -0,0 +1,15 @@
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses
+{
+    internal static partial class SkipEnabledCheckExtensions
+    {
+        [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = "Message: When using SkipEnabledCheck, the generated code skips logger.IsEnabled(logLevel) check before calling log. To be used when consumer has already guarded logger method in an IsEnabled check.", SkipEnabledCheck = true)]
+        internal static partial void LoggerMethodWithTrueSkipEnabledCheck(ILogger logger);
+
+        [LoggerMessage(EventId = 1, Level = LogLevel.Information, Message = "M1", SkipEnabledCheck = false)]
+        internal static partial void LoggerMethodWithFalseSkipEnabledCheck(ILogger logger);
+    }
+}
\ No newline at end of file