From 49881b1ddbe1331f1d198bee9f40946a72b3b70f Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 20 Nov 2025 14:41:43 -0500 Subject: [PATCH] Reduce noise from CA1873 (#51818) Several improvements to the analyzer: - Property accesses are common in logging calls, and property accesses are supposed to be cheap. Avoid raising diagnostics for property accesses. - GetType/GetHashCode/GetTimestamp are used reasonably-frequently in logging calls; special-case them to avoid diagnostics for them. - The main reason this rule exists is to eliminate cost on hot paths. Generally such hot paths aren't raising warning/error/critical diagnostics, such that the more rare warning/errors don't need as much attention to overheads. As such, I've changed the checks to only kick in by default for information and below, with a configuration switch that can be used to override to what levels it applies. --- .../docs/Analyzer Configuration.md | 25 + ...voidPotentiallyExpensiveCallWhenLogging.cs | 238 ++--- .../Options/EditorConfigOptionNames.cs | 8 + ...otentiallyExpensiveCallWhenLoggingTests.cs | 817 +++++++++++++++--- 4 files changed, 859 insertions(+), 229 deletions(-) diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/docs/Analyzer Configuration.md b/src/Microsoft.CodeAnalysis.NetAnalyzers/docs/Analyzer Configuration.md index 7a5433b3dbc2..0f8a73f4fd59 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/docs/Analyzer Configuration.md +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/docs/Analyzer Configuration.md @@ -877,6 +877,31 @@ Default Value: `false` Example: `dotnet_code_quality.CA1851.assume_method_enumerates_parameters = true` +### Maximum log level for expensive call analysis + +Option Name: `max_log_level` + +Configurable Rules: [CA1873](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/CA1873) + +Option Values: One of the following log level values (case-insensitive): + +| Option Value | Summary | +| --- | --- | +| `trace` | Analyze expensive calls at Trace level and below. | +| `debug` | Analyze expensive calls at Debug level and below. | +| `information` | Analyze expensive calls at Information level and below. | +| `warning` | Analyze expensive calls at Warning level and below. | +| `error` | Analyze expensive calls at Error level and below. | +| `critical` | Analyze expensive calls at Critical level. | + +This option configures the maximum log level for which CA1873 should flag potentially expensive operations in logging calls. Log levels higher than the configured value will not be analyzed, as they are typically always enabled in production scenarios. + +Default Value: `information` + +Example: `dotnet_code_quality.CA1873.max_log_level = warning` + +With the above configuration, the analyzer will flag expensive operations in log calls at Trace, Debug, Information, and Warning levels, but not at Error or Critical levels. + ### Proceed with analysis even if InternalsVisibleTo is present Option Name: `ignore_internalsvisibleto` diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLogging.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLogging.cs index 8806a6c0804d..4253d721e3b4 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLogging.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Microsoft.CodeAnalysis.NetAnalyzers/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLogging.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; +using System.Diagnostics; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Analyzer.Utilities.Lightup; @@ -24,24 +21,12 @@ public sealed class AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer : Diagnosti { private const string RuleId = "CA1873"; - private const string Level = nameof(Level); - private const string LogLevel = nameof(LogLevel); - - private const string Log = nameof(Log); - private const string IsEnabled = nameof(IsEnabled); - private const string LogTrace = nameof(LogTrace); - private const string LogDebug = nameof(LogDebug); - private const string LogInformation = nameof(LogInformation); - private const string LogWarning = nameof(LogWarning); - private const string LogError = nameof(LogError); - private const string LogCritical = nameof(LogCritical); - - private const int LogLevelTrace = 0; - private const int LogLevelDebug = 1; - private const int LogLevelInformation = 2; - private const int LogLevelWarning = 3; - private const int LogLevelError = 4; - private const int LogLevelCritical = 5; + private const int LogLevelTrace = 0; // LogLevel.Trace + private const int LogLevelDebug = 1; // LogLevel.Debug + private const int LogLevelInformation = 2; // LogLevel.Information + private const int LogLevelWarning = 3; // LogLevel.Warning + private const int LogLevelError = 4; // LogLevel.Error + private const int LogLevelCritical = 5; // LogLevel.Critical private const int LogLevelPassedAsParameter = int.MinValue; private static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create( @@ -54,6 +39,16 @@ public sealed class AvoidPotentiallyExpensiveCallWhenLoggingAnalyzer : Diagnosti isPortedFxCopRule: false, isDataflowRule: false); + private static readonly Dictionary s_logLevelsByName = new(StringComparer.OrdinalIgnoreCase) + { + ["trace"] = LogLevelTrace, + ["debug"] = LogLevelDebug, + ["information"] = LogLevelInformation, + ["warning"] = LogLevelWarning, + ["error"] = LogLevelError, + ["critical"] = LogLevelCritical, + }; + public sealed override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); public sealed override void Initialize(AnalysisContext context) @@ -65,7 +60,7 @@ public sealed override void Initialize(AnalysisContext context) private void OnCompilationStart(CompilationStartAnalysisContext context) { - if (!RequiredSymbols.TryGetSymbols(context.Compilation, out var symbols)) + if (RequiredSymbols.GetSymbols(context.Compilation) is not { } symbols) { return; } @@ -84,16 +79,26 @@ void AnalyzeInvocation(OperationAnalysisContext context) return; } - var arguments = invocation.Arguments.Skip(invocation.IsExtensionMethodAndHasNoInstance() ? 1 : 0); + // Check if the log level exceeds the configured maximum threshold. + if (logLevel != LogLevelPassedAsParameter && + logLevel < LogLevelCritical && + logLevel > ParseLogLevel(context.Options.GetStringOptionValue(EditorConfigOptionNames.MaxLogLevel, Rule, invocation.Syntax.SyntaxTree, context.Compilation))) + { + return; + } // Check each argument if it is potentially expensive to evaluate and raise a diagnostic if it is. - foreach (var argument in arguments) + foreach (var argument in invocation.Arguments.Skip(invocation.IsExtensionMethodAndHasNoInstance() ? 1 : 0)) { if (IsPotentiallyExpensive(argument.Value)) { context.ReportDiagnostic(argument.CreateDiagnostic(Rule)); } } + + static int ParseLogLevel(string? logLevelString) => + logLevelString is not null && s_logLevelsByName.TryGetValue(logLevelString, out var level) ? level : + LogLevelInformation; // Default to Information if invalid } } @@ -105,11 +110,17 @@ private static bool IsPotentiallyExpensive(IOperation? operation) } if (ICollectionExpressionOperationWrapper.IsInstance(operation) || - operation is IAnonymousObjectCreationOperation or - IAwaitOperation or - IInvocationOperation or - IObjectCreationOperation { Type.IsReferenceType: true } or - IWithOperation) + operation is IAnonymousObjectCreationOperation or IAwaitOperation or IWithOperation) + { + return true; + } + + if (operation is IInvocationOperation invocationOperation) + { + return !IsTrivialInvocation(invocationOperation); + } + + if (operation is IObjectCreationOperation { Type.IsReferenceType: true }) { return true; } @@ -127,19 +138,19 @@ IInvocationOperation or if (operation is IArrayElementReferenceOperation arrayElementReferenceOperation) { return IsPotentiallyExpensive(arrayElementReferenceOperation.ArrayReference) || - arrayElementReferenceOperation.Indices.Any(IsPotentiallyExpensive); + arrayElementReferenceOperation.Indices.Any(IsPotentiallyExpensive); } if (operation is IBinaryOperation binaryOperation) { return IsPotentiallyExpensive(binaryOperation.LeftOperand) || - IsPotentiallyExpensive(binaryOperation.RightOperand); + IsPotentiallyExpensive(binaryOperation.RightOperand); } if (operation is ICoalesceOperation coalesceOperation) { return IsPotentiallyExpensive(coalesceOperation.Value) || - IsPotentiallyExpensive(coalesceOperation.WhenNull); + IsPotentiallyExpensive(coalesceOperation.WhenNull); } if (operation is IConditionalAccessOperation conditionalAccessOperation) @@ -166,9 +177,11 @@ IInvocationOperation or return true; } - if (memberReferenceOperation is IPropertyReferenceOperation { Arguments.IsEmpty: false } indexerReferenceOperation) + if (memberReferenceOperation is IPropertyReferenceOperation propertyReferenceOperation) { - return indexerReferenceOperation.Arguments.Any(a => IsPotentiallyExpensive(a.Value)); + // We assume simple property accesses are cheap. For properties with arguments (indexers), + // we do still need to validate the arguments. + return propertyReferenceOperation.Arguments.Any(static a => IsPotentiallyExpensive(a.Value)); } } @@ -179,85 +192,88 @@ IInvocationOperation or return false; - static bool IsBoxing(IConversionOperation conversionOperation) + static bool IsTrivialInvocation(IInvocationOperation invocationOperation) { - var targetIsReferenceType = conversionOperation.Type?.IsReferenceType ?? false; - var operandIsValueType = conversionOperation.Operand.Type?.IsValueType ?? false; + var method = invocationOperation.TargetMethod; - return targetIsReferenceType && operandIsValueType; - } + // Special-case methods that are cheap enough we don't need to warn and + // that are reasonably common as arguments to logging methods. - static bool IsEmptyImplicitParamsArrayCreation(IArrayCreationOperation arrayCreationOperation) - { - return arrayCreationOperation.IsImplicit && - arrayCreationOperation.DimensionSizes.Length == 1 && - arrayCreationOperation.DimensionSizes[0].ConstantValue.HasValue && - arrayCreationOperation.DimensionSizes[0].ConstantValue.Value is int size && - size == 0; - } - } - - internal sealed class RequiredSymbols - { - private RequiredSymbols( - IMethodSymbol logMethod, - IMethodSymbol isEnabledMethod, - ImmutableDictionary logExtensionsMethodsAndLevel, - INamedTypeSymbol? loggerMessageAttributeType) - { - _logMethod = logMethod; - _isEnabledMethod = isEnabledMethod; - _logExtensionsMethodsAndLevel = logExtensionsMethodsAndLevel; - _loggerMessageAttributeType = loggerMessageAttributeType; - } - - public static bool TryGetSymbols(Compilation compilation, [NotNullWhen(true)] out RequiredSymbols? symbols) - { - symbols = default; - - var iLoggerType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftExtensionsLoggingILogger); - - if (iLoggerType is null) + // object.GetType / object.GetHashCode + if (method.ContainingType?.SpecialType == SpecialType.System_Object && + method.Parameters.IsEmpty && + (method.Name is nameof(GetType) or nameof(GetHashCode))) { - return false; + return true; } - var logMethod = iLoggerType.GetMembers(Log) - .OfType() - .FirstOrDefault(); - - var isEnabledMethod = iLoggerType.GetMembers(IsEnabled) - .OfType() - .FirstOrDefault(); - - if (logMethod is null || isEnabledMethod is null) + // Stopwatch.GetTimestamp + if (method.Name == nameof(Stopwatch.GetTimestamp) && + method.IsStatic && + method.Parameters.IsEmpty && + method.ContainingType?.ToDisplayString() == "System.Diagnostics.Stopwatch") { - return false; + return true; } - var loggerExtensionsType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftExtensionsLoggingLoggerExtensions); - var logExtensionsMethodsBuilder = ImmutableDictionary.CreateBuilder(SymbolEqualityComparer.Default); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(LogTrace).OfType(), LogLevelTrace); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(LogDebug).OfType(), LogLevelDebug); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(LogInformation).OfType(), LogLevelInformation); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(LogWarning).OfType(), LogLevelWarning); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(LogError).OfType(), LogLevelError); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(LogCritical).OfType(), LogLevelCritical); - AddRangeIfNotNull(logExtensionsMethodsBuilder, loggerExtensionsType?.GetMembers(Log).OfType(), LogLevelPassedAsParameter); + return false; + } + + static bool IsBoxing(IConversionOperation conversionOperation) => + conversionOperation.Type?.IsReferenceType is true && + conversionOperation.Operand.Type?.IsValueType is true; - var loggerMessageAttributeType = compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftExtensionsLoggingLoggerMessageAttribute); + static bool IsEmptyImplicitParamsArrayCreation(IArrayCreationOperation arrayCreationOperation) => + arrayCreationOperation.IsImplicit && + arrayCreationOperation.DimensionSizes.Length == 1 && + arrayCreationOperation.DimensionSizes[0].ConstantValue.HasValue && + arrayCreationOperation.DimensionSizes[0].ConstantValue.Value is int size && + size == 0; + } - symbols = new RequiredSymbols(logMethod, isEnabledMethod, logExtensionsMethodsBuilder.ToImmutable(), loggerMessageAttributeType); + internal sealed class RequiredSymbols( + IMethodSymbol logMethod, + IMethodSymbol isEnabledMethod, + Dictionary logExtensionsMethodsAndLevel, + INamedTypeSymbol? loggerMessageAttributeType) + { + private readonly IMethodSymbol _logMethod = logMethod; + private readonly IMethodSymbol _isEnabledMethod = isEnabledMethod; + private readonly Dictionary _logExtensionsMethodsAndLevel = logExtensionsMethodsAndLevel; + private readonly INamedTypeSymbol? _loggerMessageAttributeType = loggerMessageAttributeType; - return true; + public static RequiredSymbols? GetSymbols(Compilation compilation) + { + if (compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftExtensionsLoggingILogger) is not { } iLoggerType || + iLoggerType.GetMembers("Log").OfType().FirstOrDefault() is not { } logMethod || + iLoggerType.GetMembers("IsEnabled").OfType().FirstOrDefault() is not { } isEnabledMethod) + { + return null; + } - void AddRangeIfNotNull(ImmutableDictionary.Builder builder, IEnumerable? range, int value) + Dictionary logExtensionsMethods = new(SymbolEqualityComparer.Default); + if (compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftExtensionsLoggingLoggerExtensions) is { } loggerExtensionsType) { - if (range is not null) + foreach (var m in loggerExtensionsType.GetMembers().OfType()) { - builder.AddRange(range.Select(s => new KeyValuePair(s, value))); + switch (m.Name) + { + case "LogTrace": logExtensionsMethods[m] = LogLevelTrace; break; + case "LogDebug": logExtensionsMethods[m] = LogLevelDebug; break; + case "LogInformation": logExtensionsMethods[m] = LogLevelInformation; break; + case "LogWarning": logExtensionsMethods[m] = LogLevelWarning; break; + case "LogError": logExtensionsMethods[m] = LogLevelError; break; + case "LogCritical": logExtensionsMethods[m] = LogLevelCritical; break; + case "Log": logExtensionsMethods[m] = LogLevelPassedAsParameter; break; + } } } + + return new RequiredSymbols( + logMethod, + isEnabledMethod, + logExtensionsMethods, + compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftExtensionsLoggingLoggerMessageAttribute)); } public bool IsLogInvocation(IInvocationOperation invocation, out int logLevel, out IArgumentOperation? logLevelArgumentOperation) @@ -273,6 +289,11 @@ public bool IsLogInvocation(IInvocationOperation invocation, out int logLevel, o { logLevelArgumentOperation = invocation.Arguments.GetArgumentForParameterAtIndex(0); + if (logLevelArgumentOperation?.Value.ConstantValue.HasValue == true) + { + logLevel = (int)logLevelArgumentOperation.Value.ConstantValue.Value!; + } + return true; } @@ -283,33 +304,41 @@ public bool IsLogInvocation(IInvocationOperation invocation, out int logLevel, o if (logLevel == LogLevelPassedAsParameter) { logLevelArgumentOperation = invocation.Arguments.GetArgumentForParameterAtIndex(invocation.IsExtensionMethodAndHasNoInstance() ? 1 : 0); + + if (logLevelArgumentOperation?.Value.ConstantValue.HasValue == true) + { + logLevel = (int)logLevelArgumentOperation.Value.ConstantValue.Value!; + } } return true; } - var loggerMessageAttribute = method.GetAttribute(_loggerMessageAttributeType); - - if (loggerMessageAttribute is null) + if (method.GetAttribute(_loggerMessageAttributeType) is not { } loggerMessageAttribute) { return false; } // Try to get the log level from the attribute arguments. logLevel = loggerMessageAttribute.NamedArguments - .FirstOrDefault(p => p.Key.Equals(Level, StringComparison.Ordinal)) + .FirstOrDefault(p => p.Key.Equals("Level", StringComparison.Ordinal)) .Value.Value as int? ?? LogLevelPassedAsParameter; if (logLevel == LogLevelPassedAsParameter) { logLevelArgumentOperation = invocation.Arguments - .FirstOrDefault(a => a.Value.Type?.Name.Equals(LogLevel, StringComparison.Ordinal) ?? false); + .FirstOrDefault(a => a.Value.Type?.Name.Equals("LogLevel", StringComparison.Ordinal) ?? false); if (logLevelArgumentOperation is null) { return false; } + + if (logLevelArgumentOperation.Value.ConstantValue.HasValue) + { + logLevel = (int)logLevelArgumentOperation.Value.ConstantValue.Value!; + } } return true; @@ -410,11 +439,6 @@ bool IsSameLogLevel(IArgumentOperation isEnabledArgument) logLevelArgumentOperation?.Value.GetReferencedMemberOrLocalOrParameter()); } } - - private readonly IMethodSymbol _logMethod; - private readonly IMethodSymbol _isEnabledMethod; - private readonly ImmutableDictionary _logExtensionsMethodsAndLevel; - private readonly INamedTypeSymbol? _loggerMessageAttributeType; } } } diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Options/EditorConfigOptionNames.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Options/EditorConfigOptionNames.cs index 1128cfe22f4f..22e733391e8e 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Options/EditorConfigOptionNames.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/src/Utilities/Compiler/Options/EditorConfigOptionNames.cs @@ -234,5 +234,13 @@ internal static partial class EditorConfigOptionNames /// Boolean option whether to perform the analysis even if the assembly exposes its internals. /// public const string IgnoreInternalsVisibleTo = "ignore_internalsvisibleto"; + + /// + /// String option to configure the maximum log level for which expensive calls should be flagged. + /// Configurable rule: CA1873 (https://learn.microsoft.com/visualstudio/code-quality/ca1873). + /// Allowed option values: trace, debug, information, warning, error, critical. + /// Default value: information. + /// + public const string MaxLogLevel = "max_log_level"; } } diff --git a/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLoggingTests.cs b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLoggingTests.cs index 52ef278988cc..e6a6aeba9899 100644 --- a/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLoggingTests.cs +++ b/src/Microsoft.CodeAnalysis.NetAnalyzers/tests/Microsoft.CodeAnalysis.NetAnalyzers.UnitTests/Microsoft.NetCore.Analyzers/Performance/AvoidPotentiallyExpensiveCallWhenLoggingTests.cs @@ -2631,10 +2631,10 @@ void M(ILogger logger, EventId eventId, Exception exception, Func formatter, int value) + { + logger.Log(LogLevel.Debug, eventId, (long)value, exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task ReferenceTypeCast_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, string value) + { + logger.Log(LogLevel.Debug, eventId, (object)value, exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task ReferenceTypeDowncast_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, object value) + { + logger.Log(LogLevel.Debug, eventId, (string)value, exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + [Fact] public async Task BinaryOperationWithBoxing_ReportsDiagnostic_CS() { @@ -3043,7 +3094,7 @@ class C { void M(ILogger logger) { - [|logger.LogError("Test: {Number1} and {Number2}", 1, 2)|]; + [|logger.LogInformation("Test: {Number1} and {Number2}", 1, 2)|]; } } """; @@ -5261,8 +5312,8 @@ Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter A If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Trace, eventId, [|ExpensiveMethodCall()|], exception, formatter) If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Debug, [|ExpensiveMethodCall()|]) If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Warning, exception, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.[Error], eventId, exception, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Warning, exception, ExpensiveMethodCall()) + If logger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.[Error], eventId, exception, ExpensiveMethodCall()) End Sub Function ExpensiveMethodCall() As String @@ -5274,21 +5325,20 @@ End Class await VerifyBasicDiagnosticAsync(source); } - [Theory] - [MemberData(nameof(LogLevels))] - public async Task WrongLogLevelGuardedWorkInLogNamed_ReportsDiagnostic_VB(string logLevel) + [Fact] + public async Task WrongLogLevelGuardedWorkInLogNamed_ReportsDiagnostic_VB() { - string source = $$""" + string source = """ Imports System Imports Microsoft.Extensions.Logging Class C Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter As Func(Of String, Exception, String)) - If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, eventId, [|ExpensiveMethodCall()|], exception, formatter) - If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, eventId, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, exception, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, eventId, exception, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|], exception, formatter) + If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, exception, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, eventId, exception, [|ExpensiveMethodCall()|]) End Sub Function ExpensiveMethodCall() As String @@ -5300,18 +5350,17 @@ End Class await VerifyBasicDiagnosticAsync(source); } - [Theory] - [MemberData(nameof(LogLevels))] - public async Task WrongLogLevelGuardedWorkInLoggerMessage_ReportsDiagnostic_VB(string logLevel) + [Fact] + public async Task WrongLogLevelGuardedWorkInLoggerMessage_ReportsDiagnostic_VB() { - string source = $$""" + string source = """ Imports System Imports System.Runtime.CompilerServices Imports Microsoft.Extensions.Logging Partial Module C - + Partial Private Sub StaticLogLevel(logger As ILogger, argument As String) End Sub @@ -5322,7 +5371,7 @@ End Sub Sub M(logger As ILogger) If logger.IsEnabled(LogLevel.None) Then logger.StaticLogLevel([|ExpensiveMethodCall()|]) - If logger.IsEnabled(LogLevel.None) Then logger.DynamicLogLevel(LogLevel.{{logLevel}}, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(LogLevel.None) Then logger.DynamicLogLevel(LogLevel.Information, [|ExpensiveMethodCall()|]) End Sub Function ExpensiveMethodCall() As String @@ -5346,8 +5395,8 @@ Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter A If logger.IsEnabled(level) Then logger.Log(LogLevel.Trace, eventId, [|ExpensiveMethodCall()|], exception, formatter) If logger.IsEnabled(level) Then logger.Log(LogLevel.Debug, [|ExpensiveMethodCall()|]) If logger.IsEnabled(level) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(level) Then logger.Log(LogLevel.Warning, exception, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(level) Then logger.Log(LogLevel.[Error], eventId, exception, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(level) Then logger.Log(LogLevel.Warning, exception, ExpensiveMethodCall()) + If logger.IsEnabled(level) Then logger.Log(LogLevel.[Error], eventId, exception, ExpensiveMethodCall()) End Sub Function ExpensiveMethodCall() As String @@ -5359,21 +5408,20 @@ End Class await VerifyBasicDiagnosticAsync(source); } - [Theory] - [MemberData(nameof(LogLevels))] - public async Task WrongDynamicLogLevelGuardedWorkInLogNamed_ReportsDiagnostic_VB(string logLevel) + [Fact] + public async Task WrongDynamicLogLevelGuardedWorkInLogNamed_ReportsDiagnostic_VB() { - string source = $$""" + string source = """ Imports System Imports Microsoft.Extensions.Logging Class C Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter As Func(Of String, Exception, String), level As LogLevel) - If logger.IsEnabled(level) Then logger.Log(LogLevel.{{logLevel}}, eventId, [|ExpensiveMethodCall()|], exception, formatter) - If logger.IsEnabled(level) Then logger.Log(LogLevel.{{logLevel}}, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(level) Then logger.Log(LogLevel.{{logLevel}}, eventId, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(level) Then logger.Log(LogLevel.{{logLevel}}, exception, [|ExpensiveMethodCall()|]) - If logger.IsEnabled(level) Then logger.Log(LogLevel.{{logLevel}}, eventId, exception, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(level) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|], exception, formatter) + If logger.IsEnabled(level) Then logger.Log(LogLevel.Information, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(level) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(level) Then logger.Log(LogLevel.Information, exception, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(level) Then logger.Log(LogLevel.Information, eventId, exception, [|ExpensiveMethodCall()|]) End Sub Function ExpensiveMethodCall() As String @@ -5385,18 +5433,17 @@ End Class await VerifyBasicDiagnosticAsync(source); } - [Theory] - [MemberData(nameof(LogLevels))] - public async Task WrongDynamicLogLevelGuardedWorkInLoggerMessage_ReportsDiagnostic_VB(string logLevel) + [Fact] + public async Task WrongDynamicLogLevelGuardedWorkInLoggerMessage_ReportsDiagnostic_VB() { - string source = $$""" + string source = """ Imports System Imports System.Runtime.CompilerServices Imports Microsoft.Extensions.Logging Partial Module C - + Partial Private Sub StaticLogLevel(logger As ILogger, argument As String) End Sub @@ -5407,7 +5454,7 @@ End Sub Sub M(logger As ILogger, level As LogLevel) If logger.IsEnabled(level) Then logger.StaticLogLevel([|ExpensiveMethodCall()|]) - If logger.IsEnabled(level) Then logger.DynamicLogLevel(LogLevel.{{logLevel}}, [|ExpensiveMethodCall()|]) + If logger.IsEnabled(level) Then logger.DynamicLogLevel(LogLevel.Information, [|ExpensiveMethodCall()|]) End Sub Function ExpensiveMethodCall() As String @@ -5433,8 +5480,8 @@ Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter A If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Trace, eventId, [|ExpensiveMethodCall()|], exception, formatter) If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Debug, [|ExpensiveMethodCall()|]) If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|]) - If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Warning, exception, [|ExpensiveMethodCall()|]) - If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.[Error], eventId, exception, [|ExpensiveMethodCall()|]) + If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.Warning, exception, ExpensiveMethodCall()) + If _otherLogger.IsEnabled(LogLevel.Critical) Then logger.Log(LogLevel.[Error], eventId, exception, ExpensiveMethodCall()) End Sub Function ExpensiveMethodCall() As String @@ -5446,11 +5493,10 @@ End Class await VerifyBasicDiagnosticAsync(source); } - [Theory] - [MemberData(nameof(LogLevels))] - public async Task WrongInstanceGuardedWorkInLogNamed_ReportsDiagnostic_VB(string logLevel) + [Fact] + public async Task WrongInstanceGuardedWorkInLogNamed_ReportsDiagnostic_VB() { - string source = $$""" + string source = """ Imports System Imports Microsoft.Extensions.Logging @@ -5458,11 +5504,11 @@ Class C Private _otherLogger As ILogger Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter As Func(Of String, Exception, String)) - If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, eventId, [|ExpensiveMethodCall()|], exception, formatter) - If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, [|ExpensiveMethodCall()|]) - If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, eventId, [|ExpensiveMethodCall()|]) - If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, exception, [|ExpensiveMethodCall()|]) - If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.{{logLevel}}, eventId, exception, [|ExpensiveMethodCall()|]) + If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|], exception, formatter) + If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, [|ExpensiveMethodCall()|]) + If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, eventId, [|ExpensiveMethodCall()|]) + If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, exception, [|ExpensiveMethodCall()|]) + If _otherLogger.IsEnabled(LogLevel.None) Then logger.Log(LogLevel.Information, eventId, exception, [|ExpensiveMethodCall()|]) End Sub Function ExpensiveMethodCall() As String @@ -5474,11 +5520,10 @@ End Class await VerifyBasicDiagnosticAsync(source); } - [Theory] - [MemberData(nameof(LogLevels))] - public async Task WrongInstanceGuardedWorkInLoggerMessage_ReportsDiagnostic_VB(string logLevel) + [Fact] + public async Task WrongInstanceGuardedWorkInLoggerMessage_ReportsDiagnostic_VB() { - string source = $$""" + string source = """ Imports System Imports System.Runtime.CompilerServices Imports Microsoft.Extensions.Logging @@ -5487,7 +5532,7 @@ Partial Module C Private _otherLogger As ILogger - + Partial Private Sub StaticLogLevel(logger As ILogger, argument As String) End Sub @@ -5498,7 +5543,7 @@ End Sub Sub M(logger As ILogger) If _otherLogger.IsEnabled(LogLevel.None) Then logger.StaticLogLevel([|ExpensiveMethodCall()|]) - If _otherLogger.IsEnabled(LogLevel.None) Then logger.DynamicLogLevel(LogLevel.{{logLevel}}, [|ExpensiveMethodCall()|]) + If _otherLogger.IsEnabled(LogLevel.None) Then logger.DynamicLogLevel(LogLevel.Information, [|ExpensiveMethodCall()|]) End Sub Function ExpensiveMethodCall() As String @@ -5569,6 +5614,57 @@ End Class await VerifyBasicDiagnosticAsync(source); } + [Fact] + public async Task SimpleValueTypeCast_NoDiagnostic_VB() + { + string source = """ + Imports System + Imports Microsoft.Extensions.Logging + + Class C + Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter As Func(Of Long, Exception, String), value As Integer) + logger.Log(LogLevel.Debug, eventId, CLng(value), exception, formatter) + End Sub + End Class + """; + + await VerifyBasicDiagnosticAsync(source); + } + + [Fact] + public async Task ReferenceTypeCast_NoDiagnostic_VB() + { + string source = """ + Imports System + Imports Microsoft.Extensions.Logging + + Class C + Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter As Func(Of Object, Exception, String), value As String) + logger.Log(LogLevel.Debug, eventId, CObj(value), exception, formatter) + End Sub + End Class + """; + + await VerifyBasicDiagnosticAsync(source); + } + + [Fact] + public async Task ReferenceTypeDowncast_NoDiagnostic_VB() + { + string source = """ + Imports System + Imports Microsoft.Extensions.Logging + + Class C + Sub M(logger As ILogger, eventId As EventId, exception As Exception, formatter As Func(Of String, Exception, String), value As Object) + logger.Log(LogLevel.Debug, eventId, CStr(value), exception, formatter) + End Sub + End Class + """; + + await VerifyBasicDiagnosticAsync(source); + } + [Fact] public async Task BinaryOperationWithBoxing_ReportsDiagnostic_VB() { @@ -5595,7 +5691,7 @@ Imports Microsoft.Extensions.Logging Class C Sub M(logger As ILogger) - [|logger.LogError("Test: {Number1} and {Number2}", 1, 2)|] + [|logger.LogInformation("Test: {Number1} and {Number2}", 1, 2)|] End Sub End Class """; @@ -5603,28 +5699,505 @@ End Class await VerifyBasicDiagnosticAsync(source); } - // Helpers + // Tests for trivial operations that should not be flagged - private static async Task VerifyCSharpDiagnosticAsync([StringSyntax($"{LanguageNames.CSharp}-Test")] string source, CodeAnalysis.CSharp.LanguageVersion? languageVersion = null) + [Fact] + public async Task GetTypeInLog_NoDiagnostic_CS() { - await new VerifyCS.Test - { - TestCode = source, - FixedCode = source, - ReferenceAssemblies = Net60WithMELogging, - LanguageVersion = languageVersion ?? CodeAnalysis.CSharp.LanguageVersion.CSharp10 - }.RunAsync(); + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, object obj) + { + logger.Log(LogLevel.Debug, eventId, obj.GetType(), exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); } - private static async Task VerifyBasicDiagnosticAsync(string source, CodeAnalysis.VisualBasic.LanguageVersion? languageVersion = null) + [Fact] + public async Task GetTypeNameInLog_NoDiagnostic_CS() { - await new VerifyVB.Test - { - TestCode = source, - FixedCode = source, - ReferenceAssemblies = Net60WithMELogging, - LanguageVersion = languageVersion ?? CodeAnalysis.VisualBasic.LanguageVersion.VisualBasic16_9 - }.RunAsync(); + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, object obj) + { + logger.Log(LogLevel.Debug, eventId, obj.GetType().Name, exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task GetTypeFullNameInLog_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, object obj) + { + logger.Log(LogLevel.Debug, eventId, obj.GetType().FullName, exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Theory] + [MemberData(nameof(LogLevels))] + public async Task GetTypeInLogNamed_NoDiagnostic_CS(string logLevel) + { + string source = $$""" + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, object obj) + { + logger.Log{{logLevel}}(obj.GetType().Name); + logger.Log{{logLevel}}(eventId, obj.GetType().FullName); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task GetHashCodeOnReferenceTypeInLog_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, object obj) + { + logger.Log(LogLevel.Debug, eventId, obj.GetHashCode(), exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task GetHashCodeOnValueTypeInLog_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter, int value) + { + logger.Log(LogLevel.Debug, eventId, [|value.GetHashCode()|], exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task StopwatchGetTimestampInLog_NoDiagnostic_CS() + { + string source = """ + using System; + using System.Diagnostics; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Debug, eventId, Stopwatch.GetTimestamp(), exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + // Tests for LogLevel configuration + + [Fact] + public async Task InformationLevelWithDefaultConfig_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Information, eventId, [|exception.ToString()|], exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task WarningLevelWithDefaultConfig_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Warning, eventId, exception.ToString(), exception, formatter); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task WarningLevelWithConfiguredMaxWarning_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Warning, eventId, [|exception.ToString()|], exception, formatter); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = warning + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + [Fact] + public async Task ErrorLevelWithConfiguredMaxWarning_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Error, eventId, exception.ToString(), exception, formatter); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = warning + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + [Fact] + public async Task TraceLevelWithConfiguredMaxTrace_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Trace, eventId, [|exception.ToString()|], exception, formatter); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = trace + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + [Fact] + public async Task CriticalLevelWithConfiguredMaxCritical_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, EventId eventId, Exception exception, Func formatter) + { + logger.Log(LogLevel.Critical, eventId, [|exception.ToString()|], exception, formatter); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = critical + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + [Fact] + public async Task LoggerMessageInformationLevelWithDefaultConfig_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Information, Message = "Message")] + static partial void LogMethod(ILogger logger, string arg); + + void M(ILogger logger, Exception exception) + { + LogMethod(logger, [|exception.ToString()|]); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task LoggerMessageWarningLevelWithDefaultConfig_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = "Message")] + static partial void LogMethod(ILogger logger, string arg); + + void M(ILogger logger, Exception exception) + { + LogMethod(logger, exception.ToString()); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task LoggerMessageWarningLevelWithConfiguredMaxWarning_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Warning, Message = "Message")] + static partial void LogMethod(ILogger logger, string arg); + + void M(ILogger logger, Exception exception) + { + LogMethod(logger, [|exception.ToString()|]); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = warning + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + [Fact] + public async Task LoggerMessageErrorLevelWithConfiguredMaxWarning_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Error, Message = "Message")] + static partial void LogMethod(ILogger logger, string arg); + + void M(ILogger logger, Exception exception) + { + LogMethod(logger, exception.ToString()); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = warning + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + [Fact] + public async Task LoggerMessageDynamicLevelWithDefaultConfig_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + partial class C + { + [LoggerMessage(EventId = 0, Message = "Message")] + static partial void LogMethod(ILogger logger, LogLevel level, string arg); + + void M(ILogger logger, Exception exception) + { + LogMethod(logger, LogLevel.Information, [|exception.ToString()|]); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task ExtensionMethodLogInformationWithDefaultConfig_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, Exception exception) + { + logger.LogInformation([|exception.ToString()|]); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task ExtensionMethodLogWarningWithDefaultConfig_NoDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, Exception exception) + { + logger.LogWarning(exception.ToString()); + } + } + """; + + await VerifyCSharpDiagnosticAsync(source); + } + + [Fact] + public async Task ExtensionMethodLogWarningWithConfiguredMaxWarning_ReportsDiagnostic_CS() + { + string source = """ + using System; + using Microsoft.Extensions.Logging; + + class C + { + void M(ILogger logger, Exception exception) + { + logger.LogWarning([|exception.ToString()|]); + } + } + """; + + var editorconfig = ("/.editorconfig", """ + is_global = true + + dotnet_code_quality.CA1873.max_log_level = warning + """); + + await VerifyCSharpDiagnosticAsync(source, editorConfigText: editorconfig); + } + + // Helpers + + private static async Task VerifyCSharpDiagnosticAsync([StringSyntax($"{LanguageNames.CSharp}-Test")] string source, CodeAnalysis.CSharp.LanguageVersion? languageVersion = null, (string, string)? editorConfigText = null) + { + var test = new VerifyCS.Test + { + TestCode = source, + FixedCode = source, + ReferenceAssemblies = Net60WithMELogging, + LanguageVersion = languageVersion ?? CodeAnalysis.CSharp.LanguageVersion.CSharp10 + }; + + if (editorConfigText.HasValue) + { + test.TestState.AnalyzerConfigFiles.Add((editorConfigText.Value.Item1, editorConfigText.Value.Item2)); + } + + await test.RunAsync(); + } + + private static async Task VerifyBasicDiagnosticAsync(string source, CodeAnalysis.VisualBasic.LanguageVersion? languageVersion = null, (string, string)? editorConfigText = null) + { + var test = new VerifyVB.Test + { + TestCode = source, + FixedCode = source, + ReferenceAssemblies = Net60WithMELogging, + LanguageVersion = languageVersion ?? CodeAnalysis.VisualBasic.LanguageVersion.VisualBasic16_9 + }; + + if (editorConfigText.HasValue) + { + test.TestState.AnalyzerConfigFiles.Add((editorConfigText.Value.Item1, editorConfigText.Value.Item2)); + } + + await test.RunAsync(); } private static readonly ReferenceAssemblies Net60WithMELogging =