Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ You may continue using obsolete APIs in your application, but we advise explorin
| `LOGGEN004` | A static logging method must have a parameter that implements the "Microsoft.Extensions.Logging.ILogger" interface |
| `LOGGEN005` | Logging methods must be static |
| `LOGGEN006` | Logging methods must be partial |
| `LOGGEN007` | Logging methods can't be generic |
| `LOGGEN007` | Logging methods can't use the `allows ref struct` constraint |
| `LOGGEN008` | Redundant qualifier in the logging message |
| `LOGGEN009` | Don't include exception parameters as templates in the logging message |
| `LOGGEN010` | The logging template has no corresponding method parameter |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,13 @@ private void GenLogMethod(LoggingMethod lm)
OutGeneratedCodeAttribute();

OutIndent();
Out($"{lm.Modifiers} void {lm.Name}({extension}");
Out($"{lm.Modifiers} void {lm.Name}");
GenTypeParameterList(lm);
Out($"({extension}");
GenParameters(lm);
Out(")\n");
Out(')');
GenTypeConstraints(lm);
OutLn();

OutOpenBrace();

Expand Down
41 changes: 41 additions & 0 deletions src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,45 @@ internal static string PickUniqueName(string baseName, IEnumerable<string> poten
#pragma warning restore S1643 // Strings should not be concatenated using '+' in a loop
}
}

private void GenTypeParameterList(LoggingMethod lm)
{
if (lm.TypeParameters.Count == 0)
{
return;
}

bool firstItem = true;
Out('<');
foreach (var tp in lm.TypeParameters)
{
if (firstItem)
{
firstItem = false;
}
else
{
Out(", ");
}

Out(tp.Name);
}

Out('>');
}

private void GenTypeConstraints(LoggingMethod lm)
{
foreach (var tp in lm.TypeParameters)
{
if (tp.Constraints is not null)
{
OutLn();
Indent();
OutIndent();
Out($"where {tp.Name} : {tp.Constraints}");
Unindent();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal sealed class LoggingMethod
{
public readonly List<LoggingMethodParameter> Parameters = [];
public readonly List<string> Templates = [];
public readonly List<LoggingMethodTypeParameter> TypeParameters = [];
public string Name = string.Empty;
public string Message = string.Empty;
public int? Level;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Gen.Logging.Model;

/// <summary>
/// A type parameter of a generic logging method.
/// </summary>
internal sealed class LoggingMethodTypeParameter
{
public string Name = string.Empty;
public string? Constraints;
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase
messageFormat: Resources.LoggingMethodMustBePartialMessage,
category: Category);

public static DiagnosticDescriptor LoggingMethodIsGeneric { get; } = Make(
public static DiagnosticDescriptor LoggingMethodHasAllowsRefStructConstraint { get; } = Make(
id: DiagnosticIds.LoggerMessage.LOGGEN007,
title: Resources.LoggingMethodIsGenericTitle,
messageFormat: Resources.LoggingMethodIsGenericMessage,
title: Resources.LoggingMethodHasAllowsRefStructConstraintTitle,
messageFormat: Resources.LoggingMethodHasAllowsRefStructConstraintMessage,
category: Category);

public static DiagnosticDescriptor RedundantQualifierInMessage { get; } = Make(
Expand Down
67 changes: 63 additions & 4 deletions src/Generators/Microsoft.Gen.Logging/Parsing/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
Expand All @@ -16,6 +17,14 @@ namespace Microsoft.Gen.Logging.Parsing;

internal sealed partial class Parser
{
// ITypeParameterSymbol.AllowsRefLikeType was added in Roslyn 4.9 (C# 13). Access via a compiled
// delegate so the same source file compiles against all supported Roslyn versions, while
// avoiding the per-call overhead of PropertyInfo.GetValue boxing.
private static readonly Func<ITypeParameterSymbol, bool>? _getAllowsRefLikeType =
(Func<ITypeParameterSymbol, bool>?)typeof(ITypeParameterSymbol)
.GetProperty("AllowsRefLikeType")?.GetGetMethod()!
.CreateDelegate(typeof(Func<ITypeParameterSymbol, bool>));

private readonly CancellationToken _cancellationToken;
private readonly Compilation _compilation;
private readonly Action<Diagnostic> _reportDiagnostic;
Expand Down Expand Up @@ -398,11 +407,22 @@ static bool IsAllowedKind(SyntaxKind kind) =>
keepMethod = false;
}

if (method.Arity > 0)
foreach (var tp in methodSymbol.TypeParameters)
{
// we don't currently support generic methods
Diag(DiagDescriptors.LoggingMethodIsGeneric, method.TypeParameterList!.GetLocation());
keepMethod = false;
if (_getAllowsRefLikeType?.Invoke(tp) == true)
{
// 'allows ref struct' anti-constraint is not supported because the generated code stores
// parameters in fields and cannot hold ref struct type arguments.
Diag(DiagDescriptors.LoggingMethodHasAllowsRefStructConstraint, method.Identifier.GetLocation());
keepMethod = false;
break;
}

lm.TypeParameters.Add(new LoggingMethodTypeParameter
{
Name = tp.Name,
Constraints = GetTypeParameterConstraints(tp),
});
}

bool isPartial = methodSymbol.IsPartialDefinition;
Expand Down Expand Up @@ -466,6 +486,45 @@ private static bool HasXmlDocumentation(MethodDeclarationSyntax method)
return false;
}

private static string? GetTypeParameterConstraints(ITypeParameterSymbol typeParameter)
{
var constraints = new List<string>();

if (typeParameter.HasReferenceTypeConstraint)
{
string classConstraint = typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated ? "class?" : "class";
constraints.Add(classConstraint);
}
else if (typeParameter.HasValueTypeConstraint)
{
// HasUnmanagedTypeConstraint also implies HasValueTypeConstraint
constraints.Add(typeParameter.HasUnmanagedTypeConstraint ? "unmanaged" : "struct");
}
else if (typeParameter.HasNotNullConstraint)
{
constraints.Add("notnull");
}

foreach (var constraintType in typeParameter.ConstraintTypes)
{
if (constraintType is IErrorTypeSymbol)
{
continue;
}

constraints.Add(constraintType.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)));
}

if (typeParameter.HasConstructorConstraint)
{
constraints.Add("new()");
}

return constraints.Count > 0 ? string.Join(", ", constraints) : null;
}

// Returns all the classification attributes attached to a symbol.
private static List<INamedTypeSymbol> GetDataClassificationAttributes(ISymbol symbol, SymbolHolder symbols)
=> symbol
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions src/Generators/Microsoft.Gen.Logging/Parsing/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@
<data name="LoggingMethodMustBePartialMessage" xml:space="preserve">
<value>Logging methods must be partial</value>
</data>
<data name="LoggingMethodIsGenericTitle" xml:space="preserve">
<value>Logging methods can't be generic</value>
<data name="LoggingMethodHasAllowsRefStructConstraintTitle" xml:space="preserve">
<value>Logging methods can't use the 'allows ref struct' constraint</value>
</data>
<data name="LoggingMethodIsGenericMessage" xml:space="preserve">
<value>Logging methods can't be generic</value>
<data name="LoggingMethodHasAllowsRefStructConstraintMessage" xml:space="preserve">
<value>Logging methods can't use the 'allows ref struct' constraint</value>
</data>
<data name="ShouldntMentionExceptionInMessageMessage" xml:space="preserve">
<value>Don't include a template for parameter "{0}" in the logging message, exceptions are automatically delivered without being listed in the logging message</value>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.Logging;

namespace TestClasses
{
internal static partial class GenericTestExtensions
{
// generic method with single type parameter
[LoggerMessage(0, LogLevel.Debug, "M1 {value}")]
internal static partial void M1<T>(ILogger logger, T value);

// generic method with struct+Enum constraint
[LoggerMessage(1, LogLevel.Debug, "M2 {code}")]
internal static partial void M2<TCode>(ILogger logger, TCode code)
where TCode : struct, Enum;

// generic method with multiple type parameters
[LoggerMessage(2, LogLevel.Debug, "M3 {p1} {p2}")]
internal static partial void M3<T1, T2>(ILogger logger, T1 p1, T2 p2)
where T1 : class
where T2 : notnull;

// generic method with new() constraint
[LoggerMessage(3, LogLevel.Debug, "M4 {value}")]
internal static partial void M4<T>(ILogger logger, T value)
where T : new();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.Gen.Logging.Parsing;
using Xunit;

Expand Down Expand Up @@ -273,15 +274,67 @@ partial class C
[Fact]
public async Task MethodGeneric()
{
const string Source = @"
partial class C
{
[LoggerMessage(0, LogLevel.Debug, ""M1 {value}"")]
static partial void M1<T>(ILogger logger, T value);
}
";

await RunGenerator(Source);
}

[Fact]
public async Task MethodGenericWithConstraints()
{
const string Source = @"
partial class C
{
[LoggerMessage(0, LogLevel.Debug, ""M1 {code}"")]
static partial void M1<TCode>(ILogger logger, TCode code)
where TCode : struct, System.Enum;
}
";

await RunGenerator(Source);
}

[Fact]
public async Task MethodGenericMultipleTypeParams()
{
const string Source = @"
partial class C
{
[LoggerMessage(0, LogLevel.Debug, ""M1 {p1} {p2}"")]
static partial void M1<T1, T2>(ILogger logger, T1 p1, T2 p2)
where T1 : class
where T2 : notnull;
}
";

await RunGenerator(Source);
}

[Fact]
public async Task MethodGenericWithAllowsRefStructConstraint()
{
// The 'allows ref struct' detection requires Roslyn 4.9+ (ITypeParameterSymbol.AllowsRefLikeType).
// Skip gracefully on older Roslyn versions where the property and syntax are unavailable.
if (typeof(ITypeParameterSymbol).GetProperty("AllowsRefLikeType") is null)
{
return;
}

const string Source = @"
partial class C
{
[LoggerMessage(0, LogLevel.Debug, ""M1"")]
static partial void M1/*0+*/<T>/*-0*/(ILogger logger);
static partial void /*0+*/M1/*-0*/<T>(ILogger logger) where T : allows ref struct;
}
";

await RunGenerator(Source, DiagDescriptors.LoggingMethodIsGeneric);
await RunGenerator(Source, DiagDescriptors.LoggingMethodHasAllowsRefStructConstraint);
}

[Theory]
Expand Down
Loading