Skip to content

Commit 5fb8237

Browse files
authored
Added support for analyzing methods with LoggerMessage attributes (#888)
1 parent b0ff4f8 commit 5fb8237

File tree

2 files changed

+321
-2
lines changed

2 files changed

+321
-2
lines changed

src/Meziantou.Analyzer/Rules/LoggerParameterTypeAnalyzer.cs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
44
using System.Globalization;
@@ -82,6 +82,7 @@ public override void Initialize(AnalysisContext context)
8282
return;
8383

8484
context.RegisterOperationAction(ctx.AnalyzeInvocationDeclaration, OperationKind.Invocation);
85+
context.RegisterSymbolAction(ctx.AnalyzeMethodSymbol, SymbolKind.Method);
8586
});
8687
}
8788

@@ -104,6 +105,7 @@ public AnalyzerContext(CompilationStartAnalysisContext context)
104105

105106
LoggerExtensionsSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerExtensions");
106107
LoggerMessageSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessage");
108+
LoggerMessageAttributeSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessageAttribute");
107109
StructuredLogFieldAttributeSymbol = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.StructuredLogFieldAttribute");
108110

109111
SerilogLoggerEnrichmentConfigurationWithPropertySymbol = DocumentationCommentId.GetFirstSymbolForDeclarationId("M:Serilog.Configuration.LoggerEnrichmentConfiguration.WithProperty(System.String,System.Object,System.Boolean)", compilation);
@@ -230,6 +232,7 @@ static Location CreateLocation(AdditionalText file, SourceText sourceText, TextL
230232
public INamedTypeSymbol? LoggerSymbol { get; }
231233
public INamedTypeSymbol? LoggerExtensionsSymbol { get; }
232234
public INamedTypeSymbol? LoggerMessageSymbol { get; }
235+
public INamedTypeSymbol? LoggerMessageAttributeSymbol { get; }
233236

234237
public INamedTypeSymbol? SerilogLogSymbol { get; }
235238
public INamedTypeSymbol? SerilogILoggerSymbol { get; }
@@ -242,6 +245,68 @@ static Location CreateLocation(AdditionalText file, SourceText sourceText, TextL
242245

243246
public bool IsValid => Configuration.Count > 0;
244247

248+
public void AnalyzeMethodSymbol(SymbolAnalysisContext context)
249+
{
250+
var method = (IMethodSymbol)context.Symbol;
251+
252+
// Check if method has LoggerMessageAttribute
253+
var loggerMessageAttribute = method.GetAttribute(LoggerMessageAttributeSymbol);
254+
if (loggerMessageAttribute is null)
255+
return;
256+
257+
// Get the message format string from the attribute
258+
string? formatString = null;
259+
foreach (var arg in loggerMessageAttribute.ConstructorArguments)
260+
{
261+
if (arg.Type.IsString() && arg.Value is string str)
262+
{
263+
formatString = str;
264+
break;
265+
}
266+
}
267+
268+
if (string.IsNullOrEmpty(formatString))
269+
return;
270+
271+
// Parse the format string to get template parameter names
272+
var logFormat = new LogValuesFormatter(formatString);
273+
if (logFormat.ValueNames.Count == 0)
274+
return;
275+
276+
// Create a dictionary mapping parameter names to their types
277+
var parameterMap = new Dictionary<string, (ITypeSymbol Type, IParameterSymbol Parameter)>(StringComparer.OrdinalIgnoreCase);
278+
foreach (var parameter in method.Parameters)
279+
{
280+
// Skip the ILogger parameter
281+
if (parameter.Type.IsEqualTo(LoggerSymbol))
282+
continue;
283+
284+
parameterMap[parameter.Name] = (parameter.Type, parameter);
285+
}
286+
287+
// Validate each template parameter
288+
foreach (var templateParamName in logFormat.ValueNames)
289+
{
290+
if (parameterMap.TryGetValue(templateParamName, out var paramInfo))
291+
{
292+
// Validate the parameter type
293+
ValidateParameterName(context, paramInfo.Parameter, templateParamName);
294+
295+
if (!Configuration.IsValid(context.Compilation, templateParamName, paramInfo.Type, out var ruleFound))
296+
{
297+
var expectedSymbols = Configuration.GetSymbols(templateParamName) ?? [];
298+
var expectedSymbolsStr = $"must be of type {string.Join(" or ", expectedSymbols.Select(s => $"'{FormatType(s)}'"))} but is of type '{FormatType(paramInfo.Type)}'";
299+
context.ReportDiagnostic(Rule, paramInfo.Parameter, templateParamName, expectedSymbolsStr);
300+
}
301+
302+
if (!ruleFound)
303+
{
304+
context.ReportDiagnostic(RuleMissingConfiguration, paramInfo.Parameter, templateParamName);
305+
}
306+
}
307+
}
308+
}
309+
245310
public void AnalyzeInvocationDeclaration(OperationAnalysisContext context)
246311
{
247312
var operation = (IInvocationOperation)context.Operation;
@@ -440,6 +505,15 @@ private void ValidateParameterName(OperationAnalysisContext context, IOperation
440505
}
441506
}
442507

508+
private void ValidateParameterName(SymbolAnalysisContext context, IParameterSymbol parameter, string name)
509+
{
510+
var expectedSymbols = Configuration.GetSymbols(name);
511+
if (expectedSymbols is [])
512+
{
513+
context.ReportDiagnostic(Rule, parameter, name, "is not allowed by configuration");
514+
}
515+
}
516+
443517
private static string RemovePrefix(string name, char[]? potentialNamePrefixes)
444518
{
445519
if (potentialNamePrefixes is not null)

tests/Meziantou.Analyzer.Test/Rules/LoggerParameterTypeAnalyzerTests.cs

Lines changed: 246 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.Threading.Tasks;
1+
using System.Threading.Tasks;
22
using Meziantou.Analyzer.Rules;
33
using Meziantou.Analyzer.Test.Helpers;
44
using TestHelper;
@@ -475,6 +475,251 @@ await CreateProjectBuilder()
475475
.WithSourceCode(SourceCode)
476476
.AddAdditionalFile("LoggerParameterTypes.txt", """
477477
Prop;System.Int32
478+
""")
479+
.ValidateAsync();
480+
}
481+
482+
[Fact]
483+
public async Task LoggerMessageAttribute_ValidParameterTypes()
484+
{
485+
const string SourceCode = """
486+
using Microsoft.Extensions.Logging;
487+
using System.Runtime.CompilerServices;
488+
489+
partial class LoggerExtensions
490+
{
491+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
492+
static partial void LogTestMessage(ILogger logger, string Prop, int Name);
493+
}
494+
495+
class Program { static void Main() { } }
496+
""";
497+
await CreateProjectBuilder()
498+
.WithSourceCode(SourceCode)
499+
.AddAdditionalFile("LoggerParameterTypes.txt", """
500+
Prop;System.String
501+
Name;System.Int32
502+
""")
503+
.ValidateAsync();
504+
}
505+
506+
[Fact]
507+
public async Task LoggerMessageAttribute_InvalidParameterType()
508+
{
509+
const string SourceCode = """
510+
using Microsoft.Extensions.Logging;
511+
using System.Runtime.CompilerServices;
512+
513+
partial class LoggerExtensions
514+
{
515+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
516+
static partial void LogTestMessage(ILogger logger, int [|Prop|], string Name);
517+
}
518+
""";
519+
await CreateProjectBuilder()
520+
.WithSourceCode(SourceCode)
521+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
522+
.AddAdditionalFile("LoggerParameterTypes.txt", """
523+
Prop;System.String
524+
Name;System.String
525+
""")
526+
.ValidateAsync();
527+
}
528+
529+
[Fact]
530+
public async Task LoggerMessageAttribute_MultipleInvalidParameterTypes()
531+
{
532+
const string SourceCode = """
533+
using Microsoft.Extensions.Logging;
534+
using System.Runtime.CompilerServices;
535+
536+
partial class LoggerExtensions
537+
{
538+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
539+
static partial void LogTestMessage(ILogger logger, int [|Prop|], int [|Name|]);
540+
}
541+
""";
542+
await CreateProjectBuilder()
543+
.WithSourceCode(SourceCode)
544+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
545+
.AddAdditionalFile("LoggerParameterTypes.txt", """
546+
Prop;System.String
547+
Name;System.String
548+
""")
549+
.ValidateAsync();
550+
}
551+
552+
[Fact]
553+
public async Task LoggerMessageAttribute_MissingConfiguration()
554+
{
555+
const string SourceCode = """
556+
using Microsoft.Extensions.Logging;
557+
using System.Runtime.CompilerServices;
558+
559+
partial class LoggerExtensions
560+
{
561+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
562+
static partial void LogTestMessage(ILogger logger, string [|Prop|], int Name);
563+
}
564+
""";
565+
await CreateProjectBuilder()
566+
.WithSourceCode(SourceCode)
567+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
568+
.AddAdditionalFile("LoggerParameterTypes.txt", """
569+
Name;System.Int32
570+
""")
571+
.ShouldReportDiagnosticWithMessage("Log parameter 'Prop' has no configured type")
572+
.ValidateAsync();
573+
}
574+
575+
[Fact]
576+
public async Task LoggerMessageAttribute_DeniedParameter()
577+
{
578+
const string SourceCode = """
579+
using Microsoft.Extensions.Logging;
580+
using System.Runtime.CompilerServices;
581+
582+
partial class LoggerExtensions
583+
{
584+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
585+
static partial void LogTestMessage(ILogger logger, string [|Prop|], int Name);
586+
}
587+
""";
588+
await CreateProjectBuilder()
589+
.WithSourceCode(SourceCode)
590+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
591+
.AddAdditionalFile("LoggerParameterTypes.txt", """
592+
Name;System.Int32
593+
Prop;
594+
""")
595+
.ShouldReportDiagnosticWithMessage("Log parameter 'Prop' is not allowed by configuration")
596+
.ValidateAsync();
597+
}
598+
599+
[Fact]
600+
public async Task LoggerMessageAttribute_SkipILoggerParameter()
601+
{
602+
const string SourceCode = """
603+
using Microsoft.Extensions.Logging;
604+
605+
partial class LoggerExtensions
606+
{
607+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Name}")]
608+
static partial void LogTestMessage(ILogger logger, int Name);
609+
}
610+
""";
611+
await CreateProjectBuilder()
612+
.WithSourceCode(SourceCode)
613+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
614+
.AddAdditionalFile("LoggerParameterTypes.txt", """
615+
Name;System.Int32
616+
""")
617+
.ValidateAsync();
618+
}
619+
620+
[Fact]
621+
public async Task LoggerMessageAttribute_WithCallerMemberName()
622+
{
623+
const string SourceCode = """
624+
using Microsoft.Extensions.Logging;
625+
using System.Runtime.CompilerServices;
626+
627+
partial class LoggerExtensions
628+
{
629+
[LoggerMessage(10_004, LogLevel.Trace, "Test message from {Method} with {Name}")]
630+
static partial void LogTestMessage(ILogger logger, int Name, [CallerMemberName] string Method = "");
631+
}
632+
""";
633+
await CreateProjectBuilder()
634+
.WithSourceCode(SourceCode)
635+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
636+
.AddAdditionalFile("LoggerParameterTypes.txt", """
637+
Method;System.String
638+
Name;System.Int32
639+
""")
640+
.ValidateAsync();
641+
}
642+
643+
[Fact]
644+
public async Task LoggerMessageAttribute_NullableParameterType()
645+
{
646+
const string SourceCode = """
647+
using Microsoft.Extensions.Logging;
648+
649+
partial class LoggerExtensions
650+
{
651+
[LoggerMessage(10_004, LogLevel.Trace, "Test with {Value}")]
652+
static partial void LogTestMessage(ILogger logger, int Value);
653+
654+
[LoggerMessage(10_005, LogLevel.Trace, "Test with {Value}")]
655+
static partial void LogTestMessage2(ILogger logger, int? Value);
656+
}
657+
""";
658+
await CreateProjectBuilder()
659+
.WithSourceCode(SourceCode)
660+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
661+
.AddAdditionalFile("LoggerParameterTypes.txt", """
662+
Value;System.Nullable{System.Int32}
663+
""")
664+
.ValidateAsync();
665+
}
666+
667+
[Fact]
668+
public async Task LoggerMessageAttribute_NoConfiguration()
669+
{
670+
const string SourceCode = """
671+
using Microsoft.Extensions.Logging;
672+
673+
partial class LoggerExtensions
674+
{
675+
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop}")]
676+
static partial void LogTestMessage(ILogger logger, string Prop);
677+
}
678+
""";
679+
await CreateProjectBuilder()
680+
.WithSourceCode(SourceCode)
681+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
682+
.ValidateAsync();
683+
}
684+
685+
[Fact]
686+
public async Task LoggerMessageAttribute_EmptyFormatString()
687+
{
688+
const string SourceCode = """
689+
using Microsoft.Extensions.Logging;
690+
691+
partial class LoggerExtensions
692+
{
693+
[LoggerMessage(10_004, LogLevel.Trace, "")]
694+
static partial void LogTestMessage(ILogger logger);
695+
}
696+
""";
697+
await CreateProjectBuilder()
698+
.WithSourceCode(SourceCode)
699+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
700+
.AddAdditionalFile("LoggerParameterTypes.txt", """
701+
Name;System.Int32
702+
""")
703+
.ValidateAsync();
704+
}
705+
706+
[Fact]
707+
public async Task LoggerMessageAttribute_NoFormatParameters()
708+
{
709+
const string SourceCode = """
710+
using Microsoft.Extensions.Logging;
711+
712+
partial class LoggerExtensions
713+
{
714+
[LoggerMessage(10_004, LogLevel.Trace, "Test message without parameters")]
715+
static partial void LogTestMessage(ILogger logger);
716+
}
717+
""";
718+
await CreateProjectBuilder()
719+
.WithSourceCode(SourceCode)
720+
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
721+
.AddAdditionalFile("LoggerParameterTypes.txt", """
722+
Name;System.Int32
478723
""")
479724
.ValidateAsync();
480725
}

0 commit comments

Comments
 (0)