Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion src/Meziantou.Analyzer/Rules/LoggerParameterTypeAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
Expand Down Expand Up @@ -82,6 +82,7 @@ public override void Initialize(AnalysisContext context)
return;

context.RegisterOperationAction(ctx.AnalyzeInvocationDeclaration, OperationKind.Invocation);
context.RegisterSymbolAction(ctx.AnalyzeMethodSymbol, SymbolKind.Method);
});
}

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

LoggerExtensionsSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerExtensions");
LoggerMessageSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessage");
LoggerMessageAttributeSymbol = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Logging.LoggerMessageAttribute");
StructuredLogFieldAttributeSymbol = compilation.GetBestTypeByMetadataName("Meziantou.Analyzer.Annotations.StructuredLogFieldAttribute");

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

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

public bool IsValid => Configuration.Count > 0;

public void AnalyzeMethodSymbol(SymbolAnalysisContext context)
{
var method = (IMethodSymbol)context.Symbol;

// Check if method has LoggerMessageAttribute
var loggerMessageAttribute = method.GetAttribute(LoggerMessageAttributeSymbol);
if (loggerMessageAttribute is null)
return;

// Get the message format string from the attribute
string? formatString = null;
foreach (var arg in loggerMessageAttribute.ConstructorArguments)
{
if (arg.Type.IsString() && arg.Value is string str)
{
formatString = str;
break;
}
}

if (string.IsNullOrEmpty(formatString))
return;

// Parse the format string to get template parameter names
var logFormat = new LogValuesFormatter(formatString);
if (logFormat.ValueNames.Count == 0)
return;

// Create a dictionary mapping parameter names to their types
var parameterMap = new Dictionary<string, (ITypeSymbol Type, IParameterSymbol Parameter)>(StringComparer.OrdinalIgnoreCase);
foreach (var parameter in method.Parameters)
{
// Skip the ILogger parameter
if (parameter.Type.IsEqualTo(LoggerSymbol))
continue;

parameterMap[parameter.Name] = (parameter.Type, parameter);
}

// Validate each template parameter
foreach (var templateParamName in logFormat.ValueNames)
{
if (parameterMap.TryGetValue(templateParamName, out var paramInfo))
{
// Validate the parameter type
ValidateParameterName(context, paramInfo.Parameter, templateParamName);

if (!Configuration.IsValid(context.Compilation, templateParamName, paramInfo.Type, out var ruleFound))
{
var expectedSymbols = Configuration.GetSymbols(templateParamName) ?? [];
var expectedSymbolsStr = $"must be of type {string.Join(" or ", expectedSymbols.Select(s => $"'{FormatType(s)}'"))} but is of type '{FormatType(paramInfo.Type)}'";
context.ReportDiagnostic(Rule, paramInfo.Parameter, templateParamName, expectedSymbolsStr);
}

if (!ruleFound)
{
context.ReportDiagnostic(RuleMissingConfiguration, paramInfo.Parameter, templateParamName);
}
}
}
}

public void AnalyzeInvocationDeclaration(OperationAnalysisContext context)
{
var operation = (IInvocationOperation)context.Operation;
Expand Down Expand Up @@ -440,6 +505,15 @@ private void ValidateParameterName(OperationAnalysisContext context, IOperation
}
}

private void ValidateParameterName(SymbolAnalysisContext context, IParameterSymbol parameter, string name)
{
var expectedSymbols = Configuration.GetSymbols(name);
if (expectedSymbols is [])
{
context.ReportDiagnostic(Rule, parameter, name, "is not allowed by configuration");
}
}

private static string RemovePrefix(string name, char[]? potentialNamePrefixes)
{
if (potentialNamePrefixes is not null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Meziantou.Analyzer.Rules;
using Meziantou.Analyzer.Test.Helpers;
using TestHelper;
Expand Down Expand Up @@ -475,6 +475,251 @@ await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Prop;System.Int32
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_ValidParameterTypes()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
static partial void LogTestMessage(ILogger logger, string Prop, int Name);
}

class Program { static void Main() { } }
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Prop;System.String
Name;System.Int32
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_InvalidParameterType()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
static partial void LogTestMessage(ILogger logger, int [|Prop|], string Name);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Prop;System.String
Name;System.String
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_MultipleInvalidParameterTypes()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
static partial void LogTestMessage(ILogger logger, int [|Prop|], int [|Name|]);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Prop;System.String
Name;System.String
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_MissingConfiguration()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
static partial void LogTestMessage(ILogger logger, string [|Prop|], int Name);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Name;System.Int32
""")
.ShouldReportDiagnosticWithMessage("Log parameter 'Prop' has no configured type")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_DeniedParameter()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop} and {Name}")]
static partial void LogTestMessage(ILogger logger, string [|Prop|], int Name);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Name;System.Int32
Prop;
""")
.ShouldReportDiagnosticWithMessage("Log parameter 'Prop' is not allowed by configuration")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_SkipILoggerParameter()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Name}")]
static partial void LogTestMessage(ILogger logger, int Name);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Name;System.Int32
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_WithCallerMemberName()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;
using System.Runtime.CompilerServices;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message from {Method} with {Name}")]
static partial void LogTestMessage(ILogger logger, int Name, [CallerMemberName] string Method = "");
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Method;System.String
Name;System.Int32
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_NullableParameterType()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test with {Value}")]
static partial void LogTestMessage(ILogger logger, int Value);

[LoggerMessage(10_005, LogLevel.Trace, "Test with {Value}")]
static partial void LogTestMessage2(ILogger logger, int? Value);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Value;System.Nullable{System.Int32}
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_NoConfiguration()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message with {Prop}")]
static partial void LogTestMessage(ILogger logger, string Prop);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_EmptyFormatString()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "")]
static partial void LogTestMessage(ILogger logger);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Name;System.Int32
""")
.ValidateAsync();
}

[Fact]
public async Task LoggerMessageAttribute_NoFormatParameters()
{
const string SourceCode = """
using Microsoft.Extensions.Logging;

partial class LoggerExtensions
{
[LoggerMessage(10_004, LogLevel.Trace, "Test message without parameters")]
static partial void LogTestMessage(ILogger logger);
}
""";
await CreateProjectBuilder()
.WithSourceCode(SourceCode)
.WithOutputKind(Microsoft.CodeAnalysis.OutputKind.DynamicallyLinkedLibrary)
.AddAdditionalFile("LoggerParameterTypes.txt", """
Name;System.Int32
""")
.ValidateAsync();
}
Expand Down