In a .NET C# application with Microsoft logging, one will have logging statements like the following, with a message that may include brace-delimited placeholders followed by an ordered sequence of values to fill the placeholders with:
logger.LogInformation("{numRequests} orders for {item} were received in the past hour", 47, "doughnuts");
If the number of parameters after the message parameter differs from the number of placeholders, Visual Studio, flags the discrepancy. How does Visual Studio know?
Yes, I know about formattable strings, but I'm curious about what's behind the way it works in the logging methods, without formattable strings.
You can refer to this rule provided by .NET Analyzer: https://github.com/dotnet/roslyn-analyzers/blob/main/src/NetAnalyzers/Microsoft.CodeAnalysis.NetAnalyzers.sarif#L3614
Here is an example of checking whether the placeholders match the parameters in logging statements. You can see information that whether it matches validly in the console.
LoggingAnalyzer.cs
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Text.RegularExpressions;
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class LoggingAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
"LOG001",
"Logging placeholder mismatch",
"The number of placeholders in the message does not match the number of arguments",
"Logging",
DiagnosticSeverity.Error,
isEnabledByDefault: true);
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.InvocationExpression);
}
private void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
var invocation = (InvocationExpressionSyntax)context.Node;
var memberAccess = invocation.Expression as MemberAccessExpressionSyntax;
if (memberAccess == null || !memberAccess.Name.ToString().StartsWith("Log"))
return;
var arguments = invocation.ArgumentList.Arguments;
if (arguments.Count < 2)
return;
var messageArgument = arguments[0];
var messageExpression = messageArgument.Expression as LiteralExpressionSyntax;
if (messageExpression == null || messageExpression.Kind() != SyntaxKind.StringLiteralExpression)
return;
var message = messageExpression.Token.ValueText;
int placeholderCount = Regex.Matches(message, @"\{[^}]+\}").Count;
int argumentCount = arguments.Count - 1;
if (placeholderCount != argumentCount)
{
var diagnostic = Diagnostic.Create(Rule, invocation.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
Program.cs
using System;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Collections.Immutable;
class Program
{
static void Main(string[] args)
{
string code = @"
using Microsoft.Extensions.Logging;
public class TestClass
{
private ILogger<TestClass> _logger;
public TestClass(ILogger<TestClass> logger)
{
_logger = logger;
}
public void TestMethod()
{
_logger.LogInformation("" orders for {item} were received in the past hour"", 47, ""doughnuts"");
}
}";
var tree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.Create("TestAssembly", new[] { tree });
var analyzer = new LoggingAnalyzer();
var analyzers = ImmutableArray.Create<DiagnosticAnalyzer>(analyzer);
var compilationWithAnalyzers = compilation.WithAnalyzers(analyzers);
var diagnostics = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
foreach (var diagnostic in diagnostics)
{
Console.WriteLine(diagnostic);
}
}
}