diff --git a/analyzers/its/expected/Ember-MM/S2629-Ember.Plugins.json b/analyzers/its/expected/Ember-MM/S2629-Ember.Plugins.json
new file mode 100644
index 00000000000..2aab807c344
--- /dev/null
+++ b/analyzers/its/expected/Ember-MM/S2629-Ember.Plugins.json
@@ -0,0 +1,16 @@
+{
+ "Issues": [
+ {
+ "Id": "S2629",
+ "Message": "Don\u0027t use String.Format in logging message templates.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/Ember-MM/Ember.Plugins/PluginBase.cs#L71-L73",
+ "Location": "Lines 71-73 Position 27-49"
+ },
+ {
+ "Id": "S2629",
+ "Message": "Don\u0027t use String.Format in logging message templates.",
+ "Uri": "https://github.com/SonarSource/sonar-dotnet/blob/master/analyzers/its/sources/Ember-MM/Ember.Plugins/PluginSectionHandler.cs#L70",
+ "Location": "Line 70 Position 35-94"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/analyzers/rspec/cs/S2629.html b/analyzers/rspec/cs/S2629.html
new file mode 100644
index 00000000000..12bf4e0bfdc
--- /dev/null
+++ b/analyzers/rspec/cs/S2629.html
@@ -0,0 +1,38 @@
+
Why is this an issue?
+Logging arguments should not require evaluation in order to avoid unnecessary performance overhead. When passing concatenated strings or string
+interpolations directly into a logging method, the evaluation of these expressions occurs every time the logging method is called, regardless of the
+log level. This can lead to inefficient code execution and increased resource consumption.
+Instead, it is recommended to use the overload of the logger that accepts a log format and its arguments as separate parameters. By separating the
+log format from the arguments, the evaluation of expressions can be deferred until it is necessary, based on the log level. This approach improves
+performance by reducing unnecessary evaluations and ensures that logging statements are only evaluated when needed.
+Furthermore, using a constant log format enhances observability and facilitates searchability in log aggregation and monitoring software.
+The rule covers the following logging frameworks:
+
+Code examples
+Noncompliant code example
+
+public void Method(ILogger logger, bool parameter)
+{
+ logger.DebugFormat($"The value of the parameter is: {parameter}.");
+}
+
+Compliant solution
+
+public void Method(ILogger logger, bool parameter)
+{
+ logger.DebugFormat("The value of the parameter is: {Parameter}.", parameter);
+}
+
+Resources
+Documentation
+
+
diff --git a/analyzers/rspec/cs/S2629.json b/analyzers/rspec/cs/S2629.json
new file mode 100644
index 00000000000..d8e943e44fd
--- /dev/null
+++ b/analyzers/rspec/cs/S2629.json
@@ -0,0 +1,18 @@
+{
+ "title": "Logging templates should be constant",
+ "type": "CODE_SMELL",
+ "status": "ready",
+ "remediation": {
+ "func": "Constant\/Issue",
+ "constantCost": "5min"
+ },
+ "tags": [
+ "performance",
+ "logging"
+ ],
+ "defaultSeverity": "Major",
+ "ruleSpecification": "RSPEC-2629",
+ "sqKey": "S2629",
+ "scope": "Main",
+ "quickfix": "infeasible"
+}
diff --git a/analyzers/rspec/cs/Sonar_way_profile.json b/analyzers/rspec/cs/Sonar_way_profile.json
index ad3d37fb0b5..02c67d435e2 100644
--- a/analyzers/rspec/cs/Sonar_way_profile.json
+++ b/analyzers/rspec/cs/Sonar_way_profile.json
@@ -106,6 +106,7 @@
"S2583",
"S2589",
"S2612",
+ "S2629",
"S2681",
"S2688",
"S2692",
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
index e49abc905ff..97eecabf797 100644
--- a/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
+++ b/analyzers/src/SonarAnalyzer.CSharp/Extensions/SyntaxNodeExtensions.cs
@@ -162,6 +162,7 @@ public static SyntaxNode WalkUpParentheses(this SyntaxNode node)
MemberAccessExpressionSyntax { Name.Identifier: var identifier } => identifier,
MemberBindingExpressionSyntax { Name.Identifier: var identifier } => identifier,
MethodDeclarationSyntax { Identifier: var identifier } => identifier,
+ NameColonSyntax nameColon => nameColon.Name.Identifier,
NamespaceDeclarationSyntax { Name: { } name } => GetIdentifier(name),
NullableTypeSyntax { ElementType: { } elementType } => GetIdentifier(elementType),
ObjectCreationExpressionSyntax { Type: var type } => GetIdentifier(type),
diff --git a/analyzers/src/SonarAnalyzer.CSharp/Rules/UseConstantLoggingTemplate.cs b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseConstantLoggingTemplate.cs
new file mode 100644
index 00000000000..cefeee92656
--- /dev/null
+++ b/analyzers/src/SonarAnalyzer.CSharp/Rules/UseConstantLoggingTemplate.cs
@@ -0,0 +1,125 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace SonarAnalyzer.Rules.CSharp;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class UseConstantLoggingTemplate : SonarDiagnosticAnalyzer
+{
+ private const string DiagnosticId = "S2629";
+ private const string MessageFormat = "{0}";
+ private const string OnUsingStringInterpolation = "Don't use string interpolation in logging message templates.";
+ private const string OnUsingStringFormat = "Don't use String.Format in logging message templates.";
+ private const string OnUsingStringConcatenation = "Don't use string concatenation in logging message templates.";
+
+ private static readonly DiagnosticDescriptor Rule = DescriptorFactory.Create(DiagnosticId, MessageFormat);
+
+ private static readonly ImmutableDictionary Messages = new Dictionary
+ {
+ {SyntaxKind.AddExpression, OnUsingStringConcatenation},
+ {SyntaxKind.InterpolatedStringExpression, OnUsingStringInterpolation},
+ {SyntaxKind.InvocationExpression, OnUsingStringFormat},
+ }.ToImmutableDictionary();
+
+ private static readonly ImmutableArray LoggerTypes = ImmutableArray.Create(
+ KnownType.Castle_Core_Logging_ILogger,
+ KnownType.log4net_ILog,
+ KnownType.log4net_Util_ILogExtensions,
+ KnownType.Microsoft_Extensions_Logging_LoggerExtensions,
+ KnownType.NLog_ILogger,
+ KnownType.NLog_ILoggerBase,
+ KnownType.NLog_ILoggerExtensions,
+ KnownType.Serilog_ILogger,
+ KnownType.Serilog_Log);
+
+ private static readonly ImmutableHashSet LoggerMethodNames = ImmutableHashSet.Create(
+ "ConditionalDebug",
+ "ConditionalTrace",
+ "Debug",
+ "DebugFormat",
+ "Error",
+ "ErrorFormat",
+ "Fatal",
+ "FatalFormat",
+ "Info",
+ "InfoFormat",
+ "Information",
+ "Log",
+ "LogCritical",
+ "LogDebug",
+ "LogError",
+ "LogFormat",
+ "LogInformation",
+ "LogTrace",
+ "LogWarning",
+ "Trace",
+ "TraceFormat",
+ "Verbose",
+ "Warn",
+ "WarnFormat",
+ "Warning");
+
+ private static readonly ImmutableHashSet LogMessageParameterNames = ImmutableHashSet.Create(
+ "format",
+ "message",
+ "messageTemplate");
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ protected override void Initialize(SonarAnalysisContext context) =>
+ context.RegisterNodeAction(c =>
+ {
+ var invocation = (InvocationExpressionSyntax)c.Node;
+ if (LoggerMethodNames.Contains(invocation.GetName())
+ && c.SemanticModel.GetSymbolInfo(invocation).Symbol is IMethodSymbol method
+ && LoggerTypes.Any(x => x.Matches(method.ContainingType))
+ && method.Parameters.FirstOrDefault(x => LogMessageParameterNames.Contains(x.Name)) is { } messageParameter
+ && ArgumentValue(invocation, method, messageParameter) is { } argumentValue
+ && InvalidSyntaxNode(argumentValue, c.SemanticModel) is { } invalidNode)
+ {
+ c.ReportIssue(Diagnostic.Create(Rule, invalidNode.GetLocation(), Messages[invalidNode.Kind()]));
+ }
+ },
+ SyntaxKind.InvocationExpression);
+
+ private static CSharpSyntaxNode ArgumentValue(InvocationExpressionSyntax invocation, IMethodSymbol method, IParameterSymbol parameter)
+ {
+ if (invocation.ArgumentList.Arguments.FirstOrDefault(x => x.NameColon?.GetName() == parameter.Name) is { } argument)
+ {
+ return argument.Expression;
+ }
+ else
+ {
+ var paramIndex = method.Parameters.IndexOf(parameter);
+ return invocation.ArgumentList.Arguments[paramIndex].Expression;
+ }
+ }
+
+ private static SyntaxNode InvalidSyntaxNode(SyntaxNode messageArgument, SemanticModel model) =>
+ messageArgument.DescendantNodesAndSelf().FirstOrDefault(x =>
+ x.Kind() is SyntaxKind.InterpolatedStringExpression or SyntaxKind.AddExpression
+ || IsStringFormatInvocation(x, model));
+
+ private static bool IsStringFormatInvocation(SyntaxNode node, SemanticModel model) =>
+ node is InvocationExpressionSyntax invocation
+ && node.GetName() == "Format"
+ && model.GetSymbolInfo(invocation).Symbol is IMethodSymbol method
+ && KnownType.System_String.Matches(method.ContainingType);
+}
diff --git a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
index 1dd5d46e368..e8e6dee705e 100644
--- a/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
+++ b/analyzers/src/SonarAnalyzer.Common/Helpers/KnownType.cs
@@ -151,6 +151,8 @@ public sealed partial class KnownType
public static readonly KnownType NHibernate_Impl_AbstractSessionImpl = new("NHibernate.Impl.AbstractSessionImpl");
public static readonly KnownType NLog_ILogger = new("NLog.ILogger");
public static readonly KnownType NLog_ILoggerBase = new("NLog.ILoggerBase");
+ public static readonly KnownType NLog_ILoggerExtensions = new("NLog.ILoggerExtensions");
+ public static readonly KnownType NLog_Logger = new("NLog.Logger");
public static readonly KnownType NLog_LogManager = new("NLog.LogManager");
public static readonly KnownType NUnit_Framework_Assert = new("NUnit.Framework.Assert");
public static readonly KnownType NUnit_Framework_AssertionException = new("NUnit.Framework.AssertionException");
@@ -174,6 +176,8 @@ public sealed partial class KnownType
public static readonly KnownType Serilog_LoggerConfiguration = new("Serilog.LoggerConfiguration");
public static readonly KnownType ServiceStack_OrmLite_OrmLiteReadApi = new("ServiceStack.OrmLite.OrmLiteReadApi");
public static readonly KnownType ServiceStack_OrmLite_OrmLiteReadApiAsync = new("ServiceStack.OrmLite.OrmLiteReadApiAsync");
+ public static readonly KnownType Serilog_ILogger = new("Serilog.ILogger");
+ public static readonly KnownType Serilog_Log = new("Serilog.Log");
public static readonly KnownType System_Action = new("System.Action");
public static readonly KnownType System_Action_T = new("System.Action", "T");
public static readonly KnownType System_Action_T1_T2 = new("System.Action", "T1", "T2");
@@ -377,6 +381,7 @@ public sealed partial class KnownType
public static readonly KnownType System_Numerics_IEqualityOperators_TSelf_TOther_TResult = new("System.Numerics.IEqualityOperators", "TSelf", "TOther", "TResult");
public static readonly KnownType System_Numerics_IFloatingPointIeee754_TSelf = new("System.Numerics.IFloatingPointIeee754", "TSelf");
public static readonly KnownType System_Object = new("System.Object");
+ public static readonly KnownType System_Object_Array = new("System.Object") { IsArray = true };
public static readonly KnownType System_ObsoleteAttribute = new("System.ObsoleteAttribute");
public static readonly KnownType System_OutOfMemoryException = new("System.OutOfMemoryException");
public static readonly KnownType System_Random = new("System.Random");
diff --git a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
index 8668f79db2d..53331ff8e41 100644
--- a/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
+++ b/analyzers/tests/SonarAnalyzer.Test/PackagingTests/RuleTypeMappingCS.cs
@@ -2553,7 +2553,7 @@ internal static class RuleTypeMappingCS
// ["S2626"],
// ["S2627"],
// ["S2628"],
- // ["S2629"],
+ ["S2629"] = "CODE_SMELL",
// ["S2630"],
// ["S2631"],
// ["S2632"],
diff --git a/analyzers/tests/SonarAnalyzer.Test/Rules/UseConstantLoggingTemplateTest.cs b/analyzers/tests/SonarAnalyzer.Test/Rules/UseConstantLoggingTemplateTest.cs
new file mode 100644
index 00000000000..31a327290fd
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/Rules/UseConstantLoggingTemplateTest.cs
@@ -0,0 +1,177 @@
+/*
+ * SonarAnalyzer for .NET
+ * Copyright (C) 2015-2024 SonarSource SA
+ * mailto: contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+using CS = SonarAnalyzer.Rules.CSharp;
+
+namespace SonarAnalyzer.Test.Rules;
+
+[TestClass]
+public class UseConstantLoggingTemplateTest
+{
+ private readonly VerifierBuilder builder = CreateVerifier();
+
+ [TestMethod]
+ public void UseConstantLoggingTemplate_CS() =>
+ builder.AddPaths("UseConstantLoggingTemplate.cs").Verify();
+
+ [DataTestMethod]
+ [DataRow("Debug")]
+ [DataRow("Debug")]
+ [DataRow("Error")]
+ [DataRow("Fatal")]
+ [DataRow("Info")]
+ [DataRow("Trace")]
+ [DataRow("Warn")]
+ public void UseConstantLoggingTemplate_CastleCoreLogging_CS(string methodName) =>
+ builder.AddSnippet($$"""
+ using Castle.Core.Logging;
+
+ public class Program
+ {
+ public void Method(ILogger logger, int arg)
+ {
+ logger.{{methodName}}("Message"); // Compliant
+ logger.{{methodName}}($"{arg}"); // Noncompliant
+ logger.{{methodName}}Format("{Arg}", arg); // Compliant
+ logger.{{methodName}}Format($"{arg}"); // Noncompliant
+ }
+ }
+ """).Verify();
+
+ [DataTestMethod]
+ [DataRow("Debug")]
+ [DataRow("Error")]
+ [DataRow("Fatal")]
+ [DataRow("Info")]
+ [DataRow("Warn")]
+ public void UseConstantLoggingTemplate_Log4Net_CS(string methodName) =>
+ builder.AddSnippet($$"""
+ using log4net;
+
+ public class Program
+ {
+ public void Method(ILog logger, int arg)
+ {
+ logger.{{methodName}}("Message"); // Compliant
+ logger.{{methodName}}($"{arg}"); // Noncompliant
+ logger.{{methodName}}Format("Arg: {0}", arg); // Compliant
+ logger.{{methodName}}Format($"{arg}"); // Noncompliant
+ }
+ }
+ """).Verify();
+
+ [DataTestMethod]
+ [DataRow("Log", "LogLevel.Warning,")]
+ [DataRow("LogCritical")]
+ [DataRow("LogDebug")]
+ [DataRow("LogError")]
+ [DataRow("LogInformation")]
+ [DataRow("LogTrace")]
+ [DataRow("LogWarning")]
+ public void UseConstantLoggingTemplate_MicrosoftExtensionsLogging_CS(string methodName, string logLevel = "") =>
+ builder.AddSnippet($$"""
+ using Microsoft.Extensions.Logging;
+
+ public class Program
+ {
+ public void Method(ILogger logger, int arg)
+ {
+ logger.{{methodName}}({{logLevel}} "Message"); // Compliant
+ logger.{{methodName}}({{logLevel}} $"{arg}"); // Noncompliant
+ }
+ }
+ """).Verify();
+
+ [DataTestMethod]
+ [DataRow("ConditionalDebug")]
+ [DataRow("ConditionalTrace")]
+ [DataRow("Debug")]
+ [DataRow("Error")]
+ [DataRow("Fatal")]
+ [DataRow("Info")]
+ [DataRow("Log", "LogLevel.Warn,")]
+ [DataRow("Trace")]
+ [DataRow("Warn")]
+ public void UseConstantLoggingTemplate_NLog_CS(string methodName, string logLevel = "") =>
+ builder.AddSnippet($$"""
+ using NLog;
+
+ public class Program
+ {
+ public void Method(ILogger logger, int arg)
+ {
+ logger.{{methodName}}({{logLevel}} "Message"); // Compliant
+ logger.{{methodName}}({{logLevel}} $"{arg}"); // Noncompliant
+ }
+ }
+ """).Verify();
+
+ public void UseConstantLoggingTemplate_NLog_AdditionalLoggers_CS() =>
+ builder.AddSnippet("""
+ using NLog;
+
+ public class Program
+ {
+ public void Method(ILoggerBase logger, NullLogger nullLogger, int arg)
+ {
+ logger.Log(LogLevel.Warn, "Message"); // Compliant
+ logger.Log(LogLevel.Warn, $"{arg}"); // Noncompliant
+
+ nullLogger.Log(LogLevel.Warn, "Message"); // Compliant
+ nullLogger.Log(LogLevel.Warn, $"{arg}"); // Noncompliant
+ }
+ }
+ """).Verify();
+
+ [DataTestMethod]
+ [DataRow("Debug")]
+ [DataRow("Error")]
+ [DataRow("Fatal")]
+ [DataRow("Information")]
+ [DataRow("Verbose")]
+ [DataRow("Warning")]
+ public void UseConstantLoggingTemplate_Serilog_CS(string methodName) =>
+ builder.AddSnippet($$"""
+ using Serilog;
+
+ public class Program
+ {
+ public void Method(ILogger logger, int arg)
+ {
+ logger.{{methodName}}("Message without argument"); // Compliant
+ logger.{{methodName}}("The argument is {@Argument}", arg); // Compliant
+ logger.{{methodName}}($"The argument is {arg}"); // Noncompliant
+
+ Log.{{methodName}}("Message without argument"); // Compliant
+ Log.{{methodName}}("The argument is {@Argument}", arg); // Compliant
+ Log.{{methodName}}($"The argument is {arg}"); // Noncompliant
+ }
+ }
+ """).Verify();
+
+ private static VerifierBuilder CreateVerifier()
+ where TAnalyzer : DiagnosticAnalyzer, new() =>
+ new VerifierBuilder()
+ .AddReferences(NuGetMetadataReference.MicrosoftExtensionsLoggingPackages(Constants.NuGetLatestVersion))
+ .AddReferences(NuGetMetadataReference.CastleCore(Constants.NuGetLatestVersion))
+ .AddReferences(NuGetMetadataReference.Serilog(Constants.NuGetLatestVersion))
+ .AddReferences(NuGetMetadataReference.Log4Net("2.0.8", "net45-full"))
+ .AddReferences(NuGetMetadataReference.NLog(Constants.NuGetLatestVersion));
+}
diff --git a/analyzers/tests/SonarAnalyzer.Test/TestCases/UseConstantLoggingTemplate.cs b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseConstantLoggingTemplate.cs
new file mode 100644
index 00000000000..5519bebce3e
--- /dev/null
+++ b/analyzers/tests/SonarAnalyzer.Test/TestCases/UseConstantLoggingTemplate.cs
@@ -0,0 +1,90 @@
+using System;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using AliasedLogger = Microsoft.Extensions.Logging.ILogger;
+
+public class Program
+{
+ private string _stringField = "";
+ private string StringProperty => "";
+ private string StringMethod() => "";
+
+ public void BasicScenarios(ILogger logger, int arg)
+ {
+ string localVariable = "";
+
+ logger.Log(LogLevel.Warning, ""); // Compliant
+ logger.Log(LogLevel.Warning, "", 42); // Compliant - this rule doesn't care whether the additional arguments are properly used
+ logger.Log(LogLevel.Warning, "{Arg}", arg); // Compliant
+ logger.Log(LogLevel.Warning, localVariable); // Compliant
+ logger.Log(LogLevel.Warning, _stringField); // Compliant
+ logger.Log(LogLevel.Warning, StringProperty, 42); // Compliant
+ logger.Log(LogLevel.Warning, StringMethod(), 42); // Compliant
+ logger.Log(message: "{Param}", logLevel: LogLevel.Warning, args: 42); // Compliant
+
+ logger.Log(LogLevel.Warning, $"{arg}"); // Noncompliant {{Don't use string interpolation in logging message templates.}}
+ // ^^^^^^^^
+
+ logger.Log(LogLevel.Warning, "Argument: " + arg); // Noncompliant {{Don't use string concatenation in logging message templates.}}
+ // ^^^^^^^^^^^^^^^^^^
+
+ logger.Log(LogLevel.Warning, string.Format("{0}", arg)); // Noncompliant {{Don't use String.Format in logging message templates.}}
+ // ^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ logger.Log(message: $"{arg}", logLevel: LogLevel.Warning); // Noncompliant
+ logger.Log(message: (string)$"{arg}", logLevel: LogLevel.Warning); // Noncompliant
+ // ^^^^^^^^
+ logger.Log(LogLevel.Warning, (arg + " " + arg).ToLower()); // Noncompliant
+ // ^^^^^^^^^^^^^^^
+
+ logger.Log(LogLevel.Warning, new EventId(42), $"{arg}"); // Noncompliant
+ logger.Log(LogLevel.Warning, new Exception(), $"{arg}"); // Noncompliant
+ logger.Log(LogLevel.Warning, new Exception(), "{Arg}", arg); // Compliant
+
+ LoggerExtensions.Log(logger, LogLevel.Warning, "{Arg}", arg); // Compliant
+ LoggerExtensions.Log(logger, LogLevel.Warning, $"{arg}"); // Noncompliant
+ }
+
+ public void NotLoggingMethod(ILogger logger, int arg)
+ {
+ logger.BeginScope($"{arg}", 1, 2, 3); // Compliant - not a log method, the message argument is always evaluated
+ }
+
+ public void ImplementsILogger(NullLogger nullLogger, CustomLogger customLogger, int arg)
+ {
+ nullLogger.Log(LogLevel.Warning, "Arg: {Arg}", arg); // Compliant
+ nullLogger.Log(LogLevel.Warning, $"Arg: {arg}"); // Noncompliant
+ customLogger.Log(LogLevel.Warning, "Arg: {Arg}", arg); // Compliant
+ customLogger.Log(LogLevel.Warning, $"Arg: {arg}"); // Noncompliant
+ }
+
+ public void DoesNotImplementILogger(NotILogger notILogger, int arg)
+ {
+ notILogger.Log(LogLevel.Warning, "Arg: {Arg}", arg); // Compliant
+ notILogger.Log(LogLevel.Warning, $"Arg: {arg}"); // Compliant
+ }
+
+ public void AliasedILogger(AliasedLogger aliasedLogger, int arg)
+ {
+ aliasedLogger.Log(LogLevel.Warning, "Arg: {Arg}", arg); // Compliant
+ aliasedLogger.Log(LogLevel.Warning, $"Arg: {arg}"); // Noncompliant
+ }
+
+ public class CustomLogger : ILogger
+ {
+ public IDisposable BeginScope(TState state) => null;
+
+ public bool IsEnabled(LogLevel logLevel) => false;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ }
+ }
+
+ public class NotILogger
+ {
+ public void Log(LogLevel logLevel, string message, params object[] args)
+ {
+ }
+ }
+}
diff --git a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs
index 7333d9f6b4c..535e25652a8 100644
--- a/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs
+++ b/analyzers/tests/SonarAnalyzer.TestFramework/MetadataReferences/NuGetMetadataReference.cs
@@ -43,8 +43,8 @@ public static class NuGetMetadataReference
public static References AzureStorageFilesShares(string packageVersion = Constants.NuGetLatestVersion) => Create("Azure.Storage.Files.Shares", packageVersion);
public static References AzureStorageQueues(string packageVersion = Constants.NuGetLatestVersion) => Create("Azure.Storage.Queues", packageVersion);
public static References BouncyCastle(string packageVersion = "1.8.5") => Create("BouncyCastle", packageVersion);
+ public static References CastleCore(string packageVersion = "5.1.1") => Create("Castle.Core", packageVersion);
public static References Dapper(string packageVersion = "1.50.5") => Create("Dapper", packageVersion);
- public static References CastleCore(string packageVersion = Constants.NuGetLatestVersion) => Create("Castle.Core", packageVersion);
public static References CommonLoggingCore(string packageVersion = Constants.NuGetLatestVersion) => Create("Common.Logging.Core", packageVersion);
public static References EntityFramework(string packageVersion = "6.2.0") => Create("EntityFramework", packageVersion);
public static References FluentAssertions(string packageVersion) => Create("FluentAssertions", packageVersion);