Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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
38 changes: 38 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,42 @@ 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();
Out($" where {tp.Name} : {tp.Constraints}");
Comment thread
svick marked this conversation as resolved.
Outdated
}
}
}
}
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>));
Comment thread
svick marked this conversation as resolved.

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