Skip to content

Commit cee022a

Browse files
author
Sviatoslav Bychkov
committed
Added analyser for invalid template parameter names
1 parent 31c4ece commit cee022a

10 files changed

+127
-20
lines changed

src/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallExtractorTests.Extract_WithLogMethodInvocationCode_ShouldTransformThemIntoLogCallObject.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
{
3535
NativeType: global::System.String,
3636
Name: @message,
37+
Type: Message,
3738
HasPropertiesToLog: false
3839
},
3940
{

src/AutoLoggerMessageGenerator.UnitTests/Extractors/LogMessageExtractorTests.cs renamed to src/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallMessageExtractorTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
55

6-
public class LogMessageExtractorTests : BaseSourceGeneratorTest
6+
public class LogCallMessageExtractorTests : BaseSourceGeneratorTest
77
{
88
[Theory]
99
[InlineData("Hello world {1}, {2}!")]
@@ -13,7 +13,7 @@ public void Extract_WithPassingLiteralValues_ShouldReturnExpectedMessage(string
1313
var (compilation, syntaxTree) = CompileSourceCode($"""{LoggerName}.LogInformation("{expectedMessage}");""");
1414
var (invocationExpression, methodSymbol, semanticModel) = FindLoggerMethodInvocation(compilation, syntaxTree);
1515

16-
var result = LogMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
16+
var result = LogCallMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
1717

1818
result.Should().Be(expectedMessage);
1919
}
@@ -28,7 +28,7 @@ public void Extract_WithPassingConstClassMembers_ShouldReturnExpectedMessage()
2828
);
2929
var (invocationExpression, methodSymbol, semanticModel) = FindLoggerMethodInvocation(compilation, syntaxTree);
3030

31-
var result = LogMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
31+
var result = LogCallMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
3232

3333
result.Should().Be(expectedMessage);
3434
}
@@ -44,7 +44,7 @@ public void Extract_WithPassingLocalConstVariable_ShouldReturnExpectedMessage()
4444
);
4545
var (invocationExpression, methodSymbol, semanticModel) = FindLoggerMethodInvocation(compilation, syntaxTree);
4646

47-
var result = LogMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
47+
var result = LogCallMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
4848

4949
result.Should().Be(expectedMessage);
5050
}
@@ -55,7 +55,7 @@ public void Extract_WithPassingNullValues_ShouldReturnNull()
5555
var (compilation, syntaxTree) = CompileSourceCode($"{LoggerName}.LogInformation(null);");
5656
var (invocationExpression, methodSymbol, semanticModel) = FindLoggerMethodInvocation(compilation, syntaxTree);
5757

58-
var result = LogMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
58+
var result = LogCallMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
5959

6060
result.Should().Be(null);
6161
}
@@ -74,7 +74,7 @@ public void Extract_WithBinaryExpressions_ShouldReturnConcatenatedString()
7474
var (compilation, syntaxTree) = CompileSourceCode(sourceCode);
7575
var (invocationExpression, methodSymbol, semanticModel) = FindLoggerMethodInvocation(compilation, syntaxTree);
7676

77-
var result = LogMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
77+
var result = LogCallMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
7878

7979
result.Should().Be("Hello world! 42");
8080
}
@@ -93,7 +93,7 @@ public void Extract_WithBinaryNonConstantExpressions_ShouldReturnNull()
9393
var (compilation, syntaxTree) = CompileSourceCode(sourceCode);
9494
var (invocationExpression, methodSymbol, semanticModel) = FindLoggerMethodInvocation(compilation, syntaxTree);
9595

96-
var result = LogMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
96+
var result = LogCallMessageExtractor.Extract(methodSymbol!, invocationExpression, semanticModel!);
9797

9898
result.Should().Be(null);
9999
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using AutoLoggerMessageGenerator.Extractors;
2+
using FluentAssertions;
3+
4+
namespace AutoLoggerMessageGenerator.UnitTests.Extractors;
5+
6+
public class LogCallMessageParameterNamesExtractorTests
7+
{
8+
[Theory]
9+
[InlineData("Hello world {1}, {2}!", new[] { "1", "2" })]
10+
[InlineData("{param1} and {param2} are here", new[] { "param1", "param2" })]
11+
[InlineData("No parameters in this string", new string[0])]
12+
[InlineData("Edge case with empty braces {}", new[] { "" })]
13+
[InlineData("{0}{1}{2}{3}", new[] { "0", "1", "2", "3" })]
14+
[InlineData("{a} mixed with text {b}", new[] { "a", "b" })]
15+
[InlineData("double {{escaped braces}}", new[] {"{escaped braces"})]
16+
[InlineData("{name1}-{name2}-{name3}", new[] { "name1", "name2", "name3" })]
17+
[InlineData("Duplicate {param} and {param} again", new[] { "param", "param" })]
18+
[InlineData("", new string[0])]
19+
[InlineData(null, new string[0])]
20+
public void Extract_WithGivenMessage_ShouldReturnGivenMessageParameterNames(string message, params string[] expectedMessageParameters)
21+
{
22+
var result = LogCallMessageParameterNamesExtractor.Extract(message);
23+
result.Should().BeEquivalentTo(expectedMessageParameters);
24+
}
25+
}

src/AutoLoggerMessageGenerator.UnitTests/Extractors/LogCallParametersExtractorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void Extract_WithGivenMessageAndNoParameters_ShouldReturnOnlyMessageParam
7272
[Fact]
7373
public void Extract_WithUtilityParameters_ShouldExtractAllParameters()
7474
{
75-
const string message = "The {message} was processed in {Time}ms";
75+
const string message = "The {EventName} was processed in {Time}ms";
7676

7777
string extensionDeclaration = $$"""
7878
private static void Log<T1, T2>(
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using AutoLoggerMessageGenerator.Extractors;
4+
using AutoLoggerMessageGenerator.Filters;
5+
using AutoLoggerMessageGenerator.Mappers;
6+
using AutoLoggerMessageGenerator.Utilities;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
12+
namespace AutoLoggerMessageGenerator.Analysers;
13+
14+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
15+
public class InvalidTemplateParameterNameAnalyser : DiagnosticAnalyzer
16+
{
17+
private static readonly DiagnosticDescriptor Rule = new(
18+
id: "ALM001",
19+
title: "Invalid template parameter name",
20+
messageFormat: $"Template parameters ({{0}}) have invalid names ({IdentifierHelper.ValidCSharpParameterNameRegex}).",
21+
category: "Naming",
22+
defaultSeverity: DiagnosticSeverity.Warning,
23+
isEnabledByDefault: true,
24+
description: $"Template parameters in log messages must have valid names"
25+
);
26+
27+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [Rule];
28+
29+
public override void Initialize(AnalysisContext context)
30+
{
31+
context.EnableConcurrentExecution();
32+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze |
33+
GeneratedCodeAnalysisFlags.ReportDiagnostics);
34+
35+
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);
36+
}
37+
38+
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
39+
{
40+
var invocationExpression = (InvocationExpressionSyntax)context.Node;
41+
42+
if (!LogCallFilter.IsLogCallInvocation(invocationExpression, context.CancellationToken))
43+
return;
44+
45+
var semanticModel = context.SemanticModel;
46+
var methodSymbol = semanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol;
47+
if (methodSymbol is null || !LogCallFilter.IsLoggerMethod(methodSymbol))
48+
return;
49+
50+
var message = LogCallMessageExtractor.Extract(methodSymbol, invocationExpression, semanticModel);
51+
if (message is null)
52+
return;
53+
54+
var templateParameterNames = LogCallMessageParameterNamesExtractor.Extract(message)
55+
.Where(parameterName => !IdentifierHelper.IsValidCSharpParameterName(parameterName))
56+
.ToImmutableArray();
57+
58+
if (!templateParameterNames.Any())
59+
return;
60+
61+
var location = LogCallLocationMapper.Map(invocationExpression);
62+
if (location is null)
63+
return;
64+
65+
context.ReportDiagnostic(Diagnostic.Create(Rule, location.Value.Context, string.Join(", ", templateParameterNames)));
66+
}
67+
}

src/AutoLoggerMessageGenerator/Extractors/LogCallExtractor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ internal static class LogCallExtractor
2222
if (logLevel is null)
2323
return default;
2424

25-
var message = LogMessageExtractor.Extract(methodSymbol, invocationExpression, semanticModel);
25+
var message = LogCallMessageExtractor.Extract(methodSymbol, invocationExpression, semanticModel);
2626
if (message is null)
2727
return default;
2828

src/AutoLoggerMessageGenerator/Extractors/LogMessageExtractor.cs renamed to src/AutoLoggerMessageGenerator/Extractors/LogCallMessageExtractor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
namespace AutoLoggerMessageGenerator.Extractors;
77

8-
internal static class LogMessageExtractor
8+
internal static class LogCallMessageExtractor
99
{
1010
public static string? Extract(IMethodSymbol methodSymbol, InvocationExpressionSyntax invocationExpressionSyntax,
1111
SemanticModel semanticModel)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Immutable;
2+
using System.Linq;
3+
using System.Text.RegularExpressions;
4+
5+
namespace AutoLoggerMessageGenerator.Extractors;
6+
7+
internal static partial class LogCallMessageParameterNamesExtractor
8+
{
9+
public static ImmutableArray<string> Extract(string? message) =>
10+
[..MessageArgumentRegex().Matches(message ?? string.Empty).Select(c => c.Groups[1].Value)];
11+
12+
[GeneratedRegex(@"\{(.*?)\}", RegexOptions.Compiled)]
13+
private static partial Regex MessageArgumentRegex();
14+
}
15+

src/AutoLoggerMessageGenerator/Extractors/LogCallParametersExtractor.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Collections.Immutable;
22
using System.Linq;
3-
using System.Text.RegularExpressions;
43
using AutoLoggerMessageGenerator.Import.Microsoft.Extensions.Telemetry.LoggerMessage;
54
using AutoLoggerMessageGenerator.Models;
65
using AutoLoggerMessageGenerator.Utilities;
@@ -10,16 +9,14 @@
109

1110
namespace AutoLoggerMessageGenerator.Extractors;
1211

13-
internal partial class LogCallParametersExtractor(LogPropertiesCheck? logPropertiesCheck = null)
12+
internal class LogCallParametersExtractor(LogPropertiesCheck? logPropertiesCheck = null)
1413
{
1514
public ImmutableArray<LogCallParameter>? Extract(string message, IMethodSymbol methodSymbol)
1615
{
17-
var matches = MessageArgumentRegex().Matches(message);
18-
var templateParametersNames = matches.Select(c => c.Groups[1].Value)
19-
.Select(TransformParameterName)
16+
var templateParametersNames = LogCallMessageParameterNamesExtractor.Extract(message)
17+
.Select(IdentifierHelper.AddAtPrefixIfNotExists)
2018
.ToArray();
2119

22-
// TODO: Report a diagnostic message in the separate analyzer that some parameter names are "invalid"
2320
if (!templateParametersNames.All(IdentifierHelper.IsValidCSharpParameterName))
2421
return null;
2522

@@ -74,7 +71,4 @@ private static LogCallParameter CreateLogCallParameter(ITypeSymbol @nativeType,
7471

7572
private static string TransformParameterName(string parameterName) =>
7673
parameterName.StartsWith('@') ? parameterName : '@' + parameterName;
77-
78-
[GeneratedRegex(@"\{(.*?)\}", RegexOptions.Compiled)]
79-
private static partial Regex MessageArgumentRegex();
8074
}

src/AutoLoggerMessageGenerator/Utilities/IdentifierHelper.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ public static string ToValidCSharpMethodName(string? input)
1717
return sanitizedInput;
1818
}
1919

20+
public static string AddAtPrefixIfNotExists(string parameterName) =>
21+
parameterName.StartsWith('@') ? parameterName : '@' + parameterName;
22+
2023
public static bool IsValidCSharpParameterName(string name) =>
2124
!string.IsNullOrEmpty(name) && IsValidCSharpParameterNameRegex().IsMatch(name);
2225

23-
[GeneratedRegex(@"^@?[a-zA-Z_][a-zA-Z0-9_]*$")]
26+
public const string ValidCSharpParameterNameRegex = @"^@?[a-zA-Z_][a-zA-Z0-9_]*$";
27+
28+
[GeneratedRegex(ValidCSharpParameterNameRegex)]
2429
private static partial Regex IsValidCSharpParameterNameRegex();
2530
}

0 commit comments

Comments
 (0)