File: Microsoft.NetCore.Analyzers\Runtime\LoggerMessageDefineTests.cs
Web Access
Project: ..\..\..\src\Microsoft.CodeAnalysis.NetAnalyzers\tests\Microsoft.CodeAnalysis.NetAnalyzers.UnitTests\Microsoft.CodeAnalysis.NetAnalyzers.UnitTests.csproj (Microsoft.CodeAnalysis.NetAnalyzers.UnitTests)
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the MIT license.  See License.txt in the project root for license information.
 
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;
using Microsoft.NetCore.Analyzers.Runtime;
using Test.Utilities;
using Xunit;
using VerifyCS = Test.Utilities.CSharpCodeFixVerifier<
    Microsoft.NetCore.Analyzers.Runtime.LoggerMessageDefineAnalyzer,
    Microsoft.CodeAnalysis.Testing.EmptyCodeFixProvider>;
 
namespace Microsoft.Extensions.Logging.Analyzer
{
    public class LoggerMessageDefineTests
    {
        [Theory]
        [MemberData(nameof(GenerateTemplateAndDefineUsages), @"{|CA2253:""{0}""|}", "1")]
        [InlineData(@"{|CA1848:logger.LogTrace({|CA2253:""{0}""|}, 1)|};")]
        [InlineData(@"{|CA1848:logger.LogTrace({|CA2253:""{0}""|}, ""1"")|};")]
        public async Task CA2253IsProducedForNumericFormatArgumentAsync(string format)
        {
            // Make sure CA1727 is enabled for this test so we can verify it does not trigger on numeric arguments.
            await TriggerCodeAsync(format);
        }
 
        [Fact]
        public async Task CA2254ShouldNotApplyForConstantValues()
        {
            await TriggerCodeAsync("{|CA1848:logger.LogDebug($\"{nameof(System.Collections.Generic.IList<object>)}<{{Arg1}}> could not be resovled from DI container, using default JSON binder.\", typeof(string).Assembly)|};");
        }
 
        [Theory]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2254:$""{new System.Exception().Message}""|}", "11", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2254:$""{string.Empty}""|}", "11", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2254:""string"" + 2|}", "11", false)]
        public async Task CA2254IsProducedForDynamicFormatArgumentAsync(string format)
        {
            await TriggerCodeAsync(format);
        }
 
        [Theory]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:""""|}", "1", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:{|CA1727:{|CA1727:""{string} {string}""|}|}|}", "1", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:{|CA1727:{|CA1727:""{string} {string}""|}|}|}", "new object[] { 1 }", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:{|CA1727:""{string}""|}|}", "1, 2", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:{|CA1727:""{string}""|}|}", "new object[] { 1 }, new object[] { 2 }", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:{|CA1727:""{str"" + ""ing}""|}|}", "1, 2", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:""{"" + nameof(ILogger) + ""}""|}", "", true)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA2017:{|CA1727:""{"" + Const + ""}""|}|}", "", true)]
        [MemberData(nameof(GenerateDefineUsagesWithExplicitNumberOfArgs), @"{|CA2017:{|CA1727:""{string}""|}|}", 2)]
        [MemberData(nameof(GenerateDefineUsagesWithExplicitNumberOfArgs), @"{|CA2017:{|CA1727:""{str"" + ""ing}""|}|}", 2)]
        [MemberData(nameof(GenerateDefineUsagesWithExplicitNumberOfArgs), @"{|CA2017:""""|}", 1)]
        [MemberData(nameof(GenerateDefineUsagesWithExplicitNumberOfArgs), @"{|CA2017:{|CA1727:{|CA1727:""{string} {string}""|}|}|}", 1)]
        [MemberData(nameof(GenerateDefineUsagesWithExplicitNumberOfArgs), @"{|CA2017:""{"" + nameof(ILogger) + ""}""|}", 0)]
        [MemberData(nameof(GenerateDefineUsagesWithExplicitNumberOfArgs), @"{|CA2017:{|CA1727:""{"" + Const + ""}""|}|}", 0)]
        public async Task CA2017IsProducedForFormatArgumentCountMismatchAsync(string format)
        {
            await TriggerCodeAsync(format);
        }
 
        [Theory]
        [MemberData(nameof(GenerateTemplateAndDefineUsages), @"{|CA1727:""{camelCase}""|}", "1")]
        public async Task CA1727IsProducedForCamelCasedFormatArgumentAsync(string format)
        {
            await TriggerCodeAsync(format);
        }
 
        [Theory]
        // Concat would be optimized by compiler
        [MemberData(nameof(GenerateTemplateAndDefineUsageIgnoresCA1848ForBeginScope), @"nameof(ILogger) + "" string""", "")]
        [MemberData(nameof(GenerateTemplateAndDefineUsageIgnoresCA1848ForBeginScope), @""" string"" + "" string""", "")]
        [MemberData(nameof(GenerateTemplateAndDefineUsageIgnoresCA1848ForBeginScope), @"$"" string"" + $"" string""", "")]
        [MemberData(nameof(GenerateTemplateAndDefineUsages), @"{|CA1727:""{st"" + ""ring}""|}", "1")]
 
        // we are unable to parse expressions
        [MemberData(nameof(GenerateTemplateArrayUsages), @"{|CA1727:{|CA1727:""{string} {string}""|}|}", "1", false)]
        [MemberData(nameof(GenerateDefineUsages), @"{|CA1727:{|CA1727:""{string} {string}""|}|}")]
 
        // correct number of arguments
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA1727:""{string}""|}", "1", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA1727:{|CA1727:""{string} {string}""|}|}", "1, 2", false)]
        [MemberData(nameof(GenerateTemplateUsages), @"{|CA1727:{|CA1727:""{string} {string}""|}|}", "new object[] { 1, 2 }", false)]
 
        // CA2253 is not enabled by default.
        [MemberData(nameof(GenerateTemplateAndDefineUsages), @"{|CA1727:""{camelCase}""|}", "1")]
        public async Task TemplateDiagnosticsAreNotProducedAsync(string format)
        {
            await TriggerCodeAsync(format);
        }
 
        [Theory]
        [InlineData(@"LoggerMessage.Define(LogLevel.Information, 42, {|CA2017:""{One} {Two} {Three}""|});")]
        [InlineData(@"LoggerMessage.Define<int>(LogLevel.Information, 42, {|CA2017:""{One} {Two} {Three}""|});")]
        [InlineData(@"LoggerMessage.Define<int>(LogLevel.Information, 42, {|CA2017:""{One} {}""|});")]
        [InlineData(@"LoggerMessage.Define<int, int>(LogLevel.Information, 42, {|CA2017:""{One} {Two} {Three}""|});")]
        [InlineData(@"LoggerMessage.Define<int, int, int>(LogLevel.Information, 42, {|CA2017:""{One} {Two}""|});")]
        [InlineData(@"LoggerMessage.Define<int, int, int, int>(LogLevel.Information, 42, {|CA2017:""{One} {Two} {Three}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2017:""{One} {Two} {Three}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int, int>({|CA2017:""{One} {Two} {Three}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int, int, int>({|CA2017:""{One} {Two}""|});")]
        public async Task CA2017IsProducedForDefineMessageTypeParameterMismatchAsync(string expression)
        {
            await TriggerCodeAsync(expression);
        }
 
        [Theory]
        [WorkItem(7285, "https://github.com/dotnet/roslyn-analyzers/issues/7285")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""{{One}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""}{One}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""{One{Two}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""{One}{""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""{One}}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""{{{One}}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""}}{One}}""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""{{{One}{""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int>({|CA2023:""}}{One}{""|});")]
        [InlineData(@"LoggerMessage.DefineScope<int, int>({|CA2023:""}}{One} {Two}{""|});")]
        public async Task CA2023IsProducedWhenBracesAreInvalid(string format)
        {
            await TriggerCodeAsync(format);
        }
 
        [Theory]
        [InlineData("", true)]
        [InlineData(null, false)]
        [InlineData("{One}", true)]
        [InlineData("{One{", false)]
        [InlineData("{{One}", false)]
        [InlineData("{{One}}", true)]
        public void IsValidMessageTemplate_ShouldReturnExpectedResult(string messageTemplate, bool expectedResult)
        {
            var result = LoggerMessageDefineAnalyzer.IsValidMessageTemplate(messageTemplate);
 
            Assert.Equal(expectedResult, result);
        }
 
        [Theory]
        [WorkItem(7285, "https://github.com/dotnet/roslyn-analyzers/issues/7285")]
        [InlineData(@"LoggerMessage.DefineScope<int>(""Some logged value: {One}}} with an escaped brace"");")]
        [InlineData(@"LoggerMessage.DefineScope<int, int>(""}}Some logged value: {One}}} with an {Two}{{ escaped brace"");")]
        [InlineData(@"LoggerMessage.DefineScope<int, int>(""{{Some logged {{value: {One}}} with an {Two}{{{{ escaped brace{{}}"");")]
        public async Task CA2023IsNotProducedWhenBracesAreEscapedAndOtherwiseValid(string format)
        {
            await TriggerCodeAsync(format);
        }
 
        [Theory]
        [InlineData("LogTrace", @"""This is a test {Message}""")]
        [InlineData("LogDebug", @"""This is a test {Message}""")]
        [InlineData("LogInformation", @"""This is a test {Message}""")]
        [InlineData("LogWarning", @"""This is a test {Message}""")]
        [InlineData("LogError", @"""This is a test {Message}""")]
        [InlineData("LogCritical", @"""This is a test {Message}""")]
        [InlineData("BeginScope", @"""This is a test {Message}""")]
        public async Task CA1848IsProducedForInvocationsOfAllLoggerExtensionsAsync(string method, string template)
        {
            var expression = @$"{{|CA1848:logger.{method}({template},""Foo"")|}};";
            await TriggerCodeAsync(expression);
        }
 
        public static IEnumerable<object[]> GenerateTemplateAndDefineUsageIgnoresCA1848ForBeginScope(string template, string arguments)
        {
            return GenerateTemplateUsages(template, arguments, ignoreCA1848ForBeginScope: true).Concat(GenerateDefineUsages(template));
        }
 
        public static IEnumerable<object[]> GenerateTemplateAndDefineUsages(string template, string arguments)
        {
            return GenerateTemplateUsages(template, arguments, ignoreCA1848ForBeginScope: false).Concat(GenerateDefineUsages(template));
        }
 
        public static IEnumerable<object[]> GenerateDefineUsages(string template)
        {
            return GenerateDefineUsagesWithExplicitNumberOfArgs(template, numArgs: -1);
        }
 
        public static IEnumerable<object[]> GenerateDefineUsagesWithExplicitNumberOfArgs(string template, int numArgs)
        {
            TestFileMarkupParser.GetSpans(template, out _, out ImmutableDictionary<string, ImmutableArray<TextSpan>> spans);
            var spanCount = spans.Sum(pair => string.IsNullOrEmpty(pair.Key) ? 0 : pair.Value.Count());
 
            var numberOfArguments = template.Count(c => c == '{') - spanCount;
            if (numArgs != -1)
            {
                numberOfArguments = numArgs;
            }
 
            yield return new[] { $"LoggerMessage.{GenerateGenericInvocation(numberOfArguments, "DefineScope")}({template});" };
            yield return new[] { $"LoggerMessage.{GenerateGenericInvocation(numberOfArguments, "Define")}(LogLevel.Information, 42, {template});" };
        }
 
        public static IEnumerable<object[]> GenerateTemplateUsages(string template, string arguments, bool ignoreCA1848ForBeginScope)
        {
            var templateAndArguments = template;
            if (!string.IsNullOrEmpty(arguments))
            {
                templateAndArguments = $"{template}, {arguments}";
            }
 
            var methods = new[] { "LogTrace", "LogError", "LogWarning", "LogInformation", "LogDebug", "LogCritical" };
            var formats = new[]
            {
                "",
                "0, ",
                "1, new System.Exception(), ",
                "2, null, "
            };
            foreach (var method in methods)
            {
                foreach (var format in formats)
                {
                    yield return new[] { $"{{|CA1848:logger.{method}({format}{templateAndArguments})|}};" };
                }
            }
 
            yield return ignoreCA1848ForBeginScope
               ? (new[] { $"logger.BeginScope({templateAndArguments});" })
               : (new[] { $"{{|CA1848:logger.BeginScope({templateAndArguments})|}};" });
        }
 
        public static IEnumerable<object[]> GenerateTemplateArrayUsages(string template, string arguments, bool ignoreCA1848ForBeginScope)
        {
            var arrayArgsDeclaration = $"var arrayArgs = new object[] {{ {arguments} }}";
            var templateAndArguments = $"{template}, arrayArgs";
 
            var methods = new[] { "LogTrace", "LogError", "LogWarning", "LogInformation", "LogDebug", "LogCritical" };
            var formats = new[]
            {
                "",
                "0, ",
                "1, new System.Exception(), ",
                "2, null, "
            };
            foreach (var method in methods)
            {
                foreach (var format in formats)
                {
                    yield return new[] { $"{arrayArgsDeclaration};\n{{|CA1848:logger.{method}({format}{templateAndArguments})|}};" };
                }
            }
 
            yield return ignoreCA1848ForBeginScope
               ? (new[] { $"{arrayArgsDeclaration};\nlogger.BeginScope({templateAndArguments});" })
               : (new[] { $"{arrayArgsDeclaration};\n{{|CA1848:logger.BeginScope({templateAndArguments})|}};" });
        }
 
        private static string GenerateGenericInvocation(int i, string method)
        {
            if (i > 0)
            {
                var types = string.Join(", ", Enumerable.Range(0, i).Select(_ => "int"));
                method += $"<{types}>";
            }
 
            return method;
        }
 
        private async Task TriggerCodeAsync(string expression)
        {
            string code = @$"
using Microsoft.Extensions.Logging;
public class Program
{{
    public const string Const = ""const"";
    public static void Main()
    {{
        ILogger logger = null;
        {expression}
    }}
}}";
            await new VerifyCS.Test
            {
                LanguageVersion = CodeAnalysis.CSharp.LanguageVersion.CSharp9,
                TestCode = code,
                ReferenceAssemblies = AdditionalMetadataReferences.DefaultWithMELogging,
            }.RunAsync();
        }
    }
}