diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index e2176891d66..8416aa5bea7 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -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 | diff --git a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs index de0252657b7..6686888f86e 100644 --- a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs +++ b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Method.cs @@ -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(); diff --git a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Utils.cs b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Utils.cs index c550aded815..b387093ac25 100644 --- a/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Utils.cs +++ b/src/Generators/Microsoft.Gen.Logging/Emission/Emitter.Utils.cs @@ -131,4 +131,45 @@ internal static string PickUniqueName(string baseName, IEnumerable 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(); + } + } + } } diff --git a/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethod.cs b/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethod.cs index f22f1524f50..076bdb9673d 100644 --- a/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethod.cs +++ b/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethod.cs @@ -16,6 +16,7 @@ internal sealed class LoggingMethod { public readonly List Parameters = []; public readonly List Templates = []; + public readonly List TypeParameters = []; public string Name = string.Empty; public string Message = string.Empty; public int? Level; diff --git a/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodTypeParameter.cs b/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodTypeParameter.cs new file mode 100644 index 00000000000..0705baa69a2 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Logging/Model/LoggingMethodTypeParameter.cs @@ -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; + +/// +/// A type parameter of a generic logging method. +/// +internal sealed class LoggingMethodTypeParameter +{ + public string Name = string.Empty; + public string? Constraints; +} diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.Logging/Parsing/DiagDescriptors.cs index 1763581307b..b04611c7262 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/DiagDescriptors.cs +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/DiagDescriptors.cs @@ -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( diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.cs b/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.cs index 94996b7b1b3..18064fef996 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.cs +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/Parser.cs @@ -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; @@ -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? _getAllowsRefLikeType = + (Func?)typeof(ITypeParameterSymbol) + .GetProperty("AllowsRefLikeType")?.GetGetMethod()! + .CreateDelegate(typeof(Func)); + private readonly CancellationToken _cancellationToken; private readonly Compilation _compilation; private readonly Action _reportDiagnostic; @@ -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; @@ -466,6 +486,45 @@ private static bool HasXmlDocumentation(MethodDeclarationSyntax method) return false; } + private static string? GetTypeParameterConstraints(ITypeParameterSymbol typeParameter) + { + var constraints = new List(); + + 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 GetDataClassificationAttributes(ISymbol symbol, SymbolHolder symbols) => symbol diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.Designer.cs b/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.Designer.cs index 34d4e341491..4a06180f0df 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.Designer.cs +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.Designer.cs @@ -187,20 +187,20 @@ internal static string LoggingMethodHasBodyTitle { } /// - /// Looks up a localized string similar to Logging methods can't be generic. + /// Looks up a localized string similar to Logging methods can't use the 'allows ref struct' constraint. /// - internal static string LoggingMethodIsGenericMessage { + internal static string LoggingMethodHasAllowsRefStructConstraintMessage { get { - return ResourceManager.GetString("LoggingMethodIsGenericMessage", resourceCulture); + return ResourceManager.GetString("LoggingMethodHasAllowsRefStructConstraintMessage", resourceCulture); } } /// - /// Looks up a localized string similar to Logging methods can't be generic. + /// Looks up a localized string similar to Logging methods can't use the 'allows ref struct' constraint. /// - internal static string LoggingMethodIsGenericTitle { + internal static string LoggingMethodHasAllowsRefStructConstraintTitle { get { - return ResourceManager.GetString("LoggingMethodIsGenericTitle", resourceCulture); + return ResourceManager.GetString("LoggingMethodHasAllowsRefStructConstraintTitle", resourceCulture); } } diff --git a/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.resx b/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.resx index 2ce7a16851a..104c6860f95 100644 --- a/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.resx +++ b/src/Generators/Microsoft.Gen.Logging/Parsing/Resources.resx @@ -153,11 +153,11 @@ Logging methods must be partial - - Logging methods can't be generic + + Logging methods can't use the 'allows ref struct' constraint - - Logging methods can't be generic + + Logging methods can't use the 'allows ref struct' constraint Don't include a template for parameter "{0}" in the logging message, exceptions are automatically delivered without being listed in the logging message diff --git a/test/Generators/Microsoft.Gen.Logging/TestClasses/GenericTestExtensions.cs b/test/Generators/Microsoft.Gen.Logging/TestClasses/GenericTestExtensions.cs new file mode 100644 index 00000000000..f1a30a73ad0 --- /dev/null +++ b/test/Generators/Microsoft.Gen.Logging/TestClasses/GenericTestExtensions.cs @@ -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(ILogger logger, T value); + + // generic method with struct+Enum constraint + [LoggerMessage(1, LogLevel.Debug, "M2 {code}")] + internal static partial void M2(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(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(ILogger logger, T value) + where T : new(); + } +} diff --git a/test/Generators/Microsoft.Gen.Logging/Unit/ParserTests.LogMethod.cs b/test/Generators/Microsoft.Gen.Logging/Unit/ParserTests.LogMethod.cs index 9cd3c8fd252..820ffe3a9d8 100644 --- a/test/Generators/Microsoft.Gen.Logging/Unit/ParserTests.LogMethod.cs +++ b/test/Generators/Microsoft.Gen.Logging/Unit/ParserTests.LogMethod.cs @@ -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; @@ -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(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(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(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+*//*-0*/(ILogger logger); + static partial void /*0+*/M1/*-0*/(ILogger logger) where T : allows ref struct; } "; - await RunGenerator(Source, DiagDescriptors.LoggingMethodIsGeneric); + await RunGenerator(Source, DiagDescriptors.LoggingMethodHasAllowsRefStructConstraint); } [Theory]