diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index e98daf39526500..fe81c890b3f402 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -137,7 +137,7 @@ The diagnostic id values reserved for .NET Libraries analyzer warnings are `SYSL | __`SYSLIB1008`__ | One of the arguments to a logging method must implement the Microsoft.Extensions.Logging.ILogger interface | | __`SYSLIB1009`__ | Logging methods must be static | | __`SYSLIB1010`__ | Logging methods must be partial | -| __`SYSLIB1011`__ | Logging methods cannot be generic | +| __`SYSLIB1011`__ | Logging methods cannot use the `allows ref struct` constraint | | __`SYSLIB1012`__ | Redundant qualifier in logging message | | __`SYSLIB1013`__ | Don't include exception parameters as templates in the logging message | | __`SYSLIB1014`__ | Logging template has no corresponding method argument | diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/DiagnosticDescriptors.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/DiagnosticDescriptors.cs index a8f2af180720a2..1dc28cac3f7ca7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/DiagnosticDescriptors.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/DiagnosticDescriptors.cs @@ -81,10 +81,10 @@ public static class DiagnosticDescriptors DiagnosticSeverity.Error, isEnabledByDefault: true); - public static DiagnosticDescriptor LoggingMethodIsGeneric { get; } = DiagnosticDescriptorHelper.Create( + public static DiagnosticDescriptor LoggingMethodHasAllowsRefStructConstraint { get; } = DiagnosticDescriptorHelper.Create( id: "SYSLIB1011", - title: new LocalizableResourceString(nameof(SR.LoggingMethodIsGenericMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)), - messageFormat: new LocalizableResourceString(nameof(SR.LoggingMethodIsGenericMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)), + title: new LocalizableResourceString(nameof(SR.LoggingMethodHasAllowsRefStructConstraintMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)), + messageFormat: new LocalizableResourceString(nameof(SR.LoggingMethodHasAllowsRefStructConstraintMessage), SR.ResourceManager, typeof(FxResources.Microsoft.Extensions.Logging.Generators.SR)), category: "LoggingGenerator", DiagnosticSeverity.Error, isEnabledByDefault: true); diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs index 4afff41866d349..47cc3ab5a7f9b7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; @@ -60,6 +59,7 @@ public string Emit(IReadOnlyList logClasses, CancellationToken canc private static bool UseLoggerMessageDefine(LoggerMethod lm) { bool result = + (lm.TypeParameters.Count == 0) && // generic methods can't use LoggerMessage.Define's static callback (lm.TemplateParameters.Count <= MaxLoggerMessageDefineArguments) && // more args than LoggerMessage.Define can handle (lm.Level != null) && // dynamic log level, which LoggerMessage.Define can't handle (lm.TemplateList.Count == lm.TemplateParameters.Count); // mismatch in template to args, which LoggerMessage.Define can't handle @@ -146,11 +146,15 @@ namespace {lc.Namespace} private void GenStruct(LoggerMethod lm, string nestedIndentation) { - _builder.AppendLine($@" + _builder.Append($@" {nestedIndentation}/// {GeneratedTypeSummary} {nestedIndentation}[{s_generatedCodeAttribute}] {nestedIndentation}[{EditorBrowsableAttribute}] - {nestedIndentation}private readonly struct __{lm.UniqueName}Struct : global::System.Collections.Generic.IReadOnlyList> + {nestedIndentation}private readonly struct __{lm.UniqueName}Struct"); + GenTypeParameterList(lm); + _builder.Append($" : global::System.Collections.Generic.IReadOnlyList>"); + GenTypeConstraints(lm, nestedIndentation + " "); + _builder.AppendLine($@" {nestedIndentation}{{"); GenFields(lm, nestedIndentation); @@ -175,7 +179,7 @@ private void GenStruct(LoggerMethod lm, string nestedIndentation) GenVariableAssignments(lm, nestedIndentation); string formatMethodBegin = - !lm.Message.Contains('{') ? "" : + lm.Message.IndexOf('{') < 0 ? "" : _hasStringCreate ? "string.Create(global::System.Globalization.CultureInfo.InvariantCulture, " : "global::System.FormattableString.Invariant("; string formatMethodEnd = formatMethodBegin.Length > 0 ? ")" : ""; @@ -185,7 +189,9 @@ private void GenStruct(LoggerMethod lm, string nestedIndentation) {nestedIndentation}}} "); _builder.Append($@" - {nestedIndentation}public static readonly global::System.Func<__{lm.UniqueName}Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString(); + {nestedIndentation}public static readonly global::System.Func<__{lm.UniqueName}Struct"); + GenTypeParameterList(lm); + _builder.Append($@", global::System.Exception?, string> Format = (state, ex) => state.ToString(); {nestedIndentation}public int Count => {lm.TemplateParameters.Count + 1}; @@ -369,9 +375,9 @@ private void GenArguments(LoggerMethod lm) private void GenHolder(LoggerMethod lm) { - string typeName = $"__{lm.UniqueName}Struct"; - - _builder.Append($"new {typeName}("); + _builder.Append($"new __{lm.UniqueName}Struct"); + GenTypeParameterList(lm); + _builder.Append('('); foreach (LoggerParameter p in lm.TemplateParameters) { if (p != lm.TemplateParameters[0]) @@ -385,6 +391,44 @@ private void GenHolder(LoggerMethod lm) _builder.Append(')'); } + private void GenTypeParameterList(LoggerMethod lm) + { + if (lm.TypeParameters.Count == 0) + { + return; + } + + _builder.Append('<'); + bool firstItem = true; + foreach (LoggerMethodTypeParameter tp in lm.TypeParameters) + { + if (firstItem) + { + firstItem = false; + } + else + { + _builder.Append(", "); + } + + _builder.Append(tp.Name); + } + + _builder.Append('>'); + } + + private void GenTypeConstraints(LoggerMethod lm, string nestedIndentation) + { + foreach (LoggerMethodTypeParameter tp in lm.TypeParameters) + { + if (tp.Constraints is not null) + { + _builder.Append(@$" + {nestedIndentation}where {tp.Name} : {tp.Constraints}"); + } + } + } + private void GenLogMethod(LoggerMethod lm, string nestedIndentation) { string level = GetLogLevel(lm); @@ -414,11 +458,15 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation) _builder.Append($@" {nestedIndentation}[{s_generatedCodeAttribute}] - {nestedIndentation}{lm.Modifiers} void {lm.Name}({extension}"); + {nestedIndentation}{lm.Modifiers} void {lm.Name}"); + GenTypeParameterList(lm); + _builder.Append($"({extension}"); GenParameters(lm); - _builder.Append($@") + _builder.Append(')'); + GenTypeConstraints(lm, nestedIndentation); + _builder.Append($@" {nestedIndentation}{{"); string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : " "; @@ -448,7 +496,9 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation) GenHolder(lm); _builder.Append($@", {nestedIndentation}{enabledCheckIndentation}{exceptionArg}, - {nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Struct.Format);"); + {nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Struct"); + GenTypeParameterList(lm); + _builder.Append(".Format);"); } if (!lm.SkipEnabledCheck) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs index 6af2a7856ed41e..e2a265dbb270dc 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs @@ -22,6 +22,13 @@ internal sealed class Parser { internal const string LoggerMessageAttribute = "Microsoft.Extensions.Logging.LoggerMessageAttribute"; + // 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? s_getAllowsRefLikeType = + (Func?) + typeof(ITypeParameterSymbol).GetProperty("AllowsRefLikeType")?.GetGetMethod()!.CreateDelegate(typeof(Func)); + private readonly CancellationToken _cancellationToken; private readonly INamedTypeSymbol _loggerMessageAttribute; private readonly INamedTypeSymbol _loggerSymbol; @@ -239,8 +246,29 @@ public IReadOnlyList GetLogClasses(IEnumerable GetLogClasses(IEnumerable 0) - { - // we don't currently support generic methods - Diag(DiagnosticDescriptors.LoggingMethodIsGeneric, method.Identifier.GetLocation()); - keepMethod = false; - } - bool isStatic = false; bool isPartial = false; foreach (SyntaxToken mod in method.Modifiers) @@ -369,6 +390,7 @@ public IReadOnlyList GetLogClasses(IEnumerable(); + + 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 (ITypeSymbol 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; + } + private void Diag(DiagnosticDescriptor desc, Location? location, params object?[]? messageArgs) { // Report immediately if callback is provided (preserves pragma suppression with original locations) @@ -918,6 +982,7 @@ internal sealed class LoggerMethod public readonly List TemplateParameters = new(); public readonly Dictionary TemplateMap = new(StringComparer.OrdinalIgnoreCase); public readonly List TemplateList = new(); + public readonly List TypeParameters = new(); public string Name = string.Empty; public string UniqueName = string.Empty; public string Message = string.Empty; @@ -935,6 +1000,7 @@ internal sealed class LoggerMethod TemplateParameters = TemplateParameters.Select(p => p.ToSpec()).ToImmutableEquatableArray(), TemplateMap = TemplateMap.Select(kvp => new KeyValuePairEquatable(kvp.Key, kvp.Value)).ToImmutableEquatableArray(), TemplateList = TemplateList.ToImmutableEquatableArray(), + TypeParameters = TypeParameters.Select(tp => tp.ToSpec()).ToImmutableEquatableArray(), Name = Name, UniqueName = UniqueName, Message = Message, @@ -957,6 +1023,7 @@ internal sealed record LoggerMethodSpec : IEquatable public required ImmutableEquatableArray TemplateParameters { get; init; } public required ImmutableEquatableArray> TemplateMap { get; init; } public required ImmutableEquatableArray TemplateList { get; init; } + public required ImmutableEquatableArray TypeParameters { get; init; } public required string Name { get; init; } public required string UniqueName { get; init; } public required string Message { get; init; } @@ -976,6 +1043,7 @@ public bool Equals(LoggerMethodSpec? other) TemplateParameters.Equals(other.TemplateParameters) && TemplateMap.Equals(other.TemplateMap) && TemplateList.Equals(other.TemplateList) && + TypeParameters.Equals(other.TypeParameters) && Name == other.Name && UniqueName == other.UniqueName && Message == other.Message && @@ -994,6 +1062,7 @@ public override int GetHashCode() hash = HashHelpers.Combine(hash, TemplateParameters.GetHashCode()); hash = HashHelpers.Combine(hash, TemplateMap.GetHashCode()); hash = HashHelpers.Combine(hash, TemplateList.GetHashCode()); + hash = HashHelpers.Combine(hash, TypeParameters.GetHashCode()); hash = HashHelpers.Combine(hash, Name.GetHashCode()); hash = HashHelpers.Combine(hash, UniqueName.GetHashCode()); hash = HashHelpers.Combine(hash, Message.GetHashCode()); @@ -1102,6 +1171,44 @@ public override int GetHashCode() } } + /// + /// A type parameter of a generic logging method. + /// + internal sealed class LoggerMethodTypeParameter + { + public string Name = string.Empty; + public string? Constraints; + + public LoggerMethodTypeParameterSpec ToSpec() => new LoggerMethodTypeParameterSpec + { + Name = Name, + Constraints = Constraints + }; + } + + /// + /// Immutable specification of a type parameter for incremental caching. + /// + internal sealed record LoggerMethodTypeParameterSpec : IEquatable + { + public required string Name { get; init; } + public required string? Constraints { get; init; } + + public bool Equals(LoggerMethodTypeParameterSpec? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Name == other.Name && Constraints == other.Constraints; + } + + public override int GetHashCode() + { + int hash = Name.GetHashCode(); + hash = HashHelpers.Combine(hash, Constraints?.GetHashCode() ?? 0); + return hash; + } + } + /// /// Returns a non-randomized hash code for the given string. /// We always return a positive value. diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs index 40b7f73b6bf98e..19e1714bcf295b 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Roslyn4.0.cs @@ -259,6 +259,15 @@ private static LoggerClass FromSpec(LoggerClassSpec spec) lm.TemplateList.Add(template); } + foreach (var typeParamSpec in methodSpec.TypeParameters) + { + lm.TypeParameters.Add(new LoggerMethodTypeParameter + { + Name = typeParamSpec.Name, + Constraints = typeParamSpec.Constraints + }); + } + lc.Methods.Add(lm); } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/Strings.resx index cca9ce4ef6c0e0..518b56f86de8eb 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/Strings.resx @@ -152,8 +152,8 @@ Logging methods must be partial - - Logging methods cannot be generic + + Logging methods cannot have type parameters with 'allows ref struct' constraints Don't include a template for {0} in the logging message since it is implicitly taken care of diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.cs.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.cs.xlf index db9efa93236cc2..c8c34611ca8934 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.cs.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.cs.xlf @@ -47,9 +47,9 @@ Metody protokolování nemůžou obsahovat tělo. - - Logging methods cannot be generic - Metody protokolování nemůžou být obecné. + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Metody protokolování nemůžou být obecné. diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.de.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.de.xlf index 1a557ac1669fcd..c0358934569642 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.de.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.de.xlf @@ -47,9 +47,9 @@ Protokollierungsmethoden dürfen keinen Text enthalten. - - Logging methods cannot be generic - Protokollierungsmethoden können nicht generisch sein. + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Protokollierungsmethoden können nicht generisch sein. diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.es.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.es.xlf index c67ec41ff91cc8..289d568c983348 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.es.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.es.xlf @@ -47,9 +47,9 @@ Los métodos de registro no pueden tener cuerpo - - Logging methods cannot be generic - Los métodos de registro no pueden ser genéricos + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Los métodos de registro no pueden ser genéricos diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.fr.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.fr.xlf index bb73608d9d0382..03e6a33117bf5d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.fr.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.fr.xlf @@ -47,9 +47,9 @@ Les méthodes de journalisation ne peuvent pas avoir de corps - - Logging methods cannot be generic - Les méthodes de journalisation ne peuvent pas être génériques + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Les méthodes de journalisation ne peuvent pas être génériques diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.it.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.it.xlf index d4bd8eb5a895c3..f4683d093b4a00 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.it.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.it.xlf @@ -47,9 +47,9 @@ I metodi di registrazione non possono avere un corpo - - Logging methods cannot be generic - I metodi di registrazione non possono essere generici + + Logging methods cannot have type parameters with 'allows ref struct' constraints + I metodi di registrazione non possono essere generici diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ja.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ja.xlf index 1f19c136e50db5..e95982c3a3793a 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ja.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ja.xlf @@ -47,9 +47,9 @@ ログ メソッドは本文を含めることができません - - Logging methods cannot be generic - ログ メソッドを汎用にすることはできません + + Logging methods cannot have type parameters with 'allows ref struct' constraints + ログ メソッドを汎用にすることはできません diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ko.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ko.xlf index 94a4654cf41a68..b046b5923431bf 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ko.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ko.xlf @@ -47,9 +47,9 @@ 로깅 메서드에는 본문을 사용할 수 없음 - - Logging methods cannot be generic - 로깅 메서드는 제네릭일 수 없음 + + Logging methods cannot have type parameters with 'allows ref struct' constraints + 로깅 메서드는 제네릭일 수 없음 diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pl.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pl.xlf index 48dcc267ff34f2..68b11c6daf9aa1 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pl.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pl.xlf @@ -47,9 +47,9 @@ Metody rejestrowania nie mogą mieć treści - - Logging methods cannot be generic - Metody rejestrowania nie mogą być ogólne + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Metody rejestrowania nie mogą być ogólne diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pt-BR.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pt-BR.xlf index c698b2fc61d947..436da73b7ba53d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pt-BR.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.pt-BR.xlf @@ -47,9 +47,9 @@ Os métodos de registro em log não podem ter um corpo - - Logging methods cannot be generic - Os métodos de registro em log não podem ser genéricos + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Os métodos de registro em log não podem ser genéricos diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ru.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ru.xlf index 4e59b9fd9b04a5..5ac98610c158e3 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ru.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.ru.xlf @@ -47,9 +47,9 @@ У методов ведения журнала не может быть текста - - Logging methods cannot be generic - Методы ведения журнала не могут быть универсальными + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Методы ведения журнала не могут быть универсальными diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.tr.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.tr.xlf index b92fc16b8c2aba..d8f374451eaf73 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.tr.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.tr.xlf @@ -47,9 +47,9 @@ Günlüğe kaydetme yöntemleri gövde içeremez - - Logging methods cannot be generic - Günlüğe kaydetme yöntemleri genel olamaz + + Logging methods cannot have type parameters with 'allows ref struct' constraints + Günlüğe kaydetme yöntemleri genel olamaz diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hans.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hans.xlf index 6f70cbcdd9b34e..105b5efe076545 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hans.xlf @@ -47,9 +47,9 @@ 日志记录方法不能有正文 - - Logging methods cannot be generic - 日志记录方法不能为泛型方法 + + Logging methods cannot have type parameters with 'allows ref struct' constraints + 日志记录方法不能为泛型方法 diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hant.xlf b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hant.xlf index 810bdfe157fede..7ab0aa33ced963 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/Resources/xlf/Strings.zh-Hant.xlf @@ -47,9 +47,9 @@ 記錄方法不能有主體 - - Logging methods cannot be generic - 記錄方法不可為泛型 + + Logging methods cannot have type parameters with 'allows ref struct' constraints + 記錄方法不可為泛型 diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithGenericMethods.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithGenericMethods.generated.txt new file mode 100644 index 00000000000000..29b38449fe5a84 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithGenericMethods.generated.txt @@ -0,0 +1,208 @@ +// +#nullable enable + +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + partial class TestWithGenericMethods + { + /// This API supports the logging infrastructure and is not intended to be used directly from your code. It is subject to change in the future. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + private readonly struct __M0Struct : global::System.Collections.Generic.IReadOnlyList> + where TCode : struct, global::System.Enum + { + private readonly TCode _code; + + public __M0Struct(TCode code) + { + this._code = code; + + } + + public override string ToString() + { + var code = this._code; + + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"M0 {code}"); + } + + public static readonly global::System.Func<__M0Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString(); + + public int Count => 2; + + public global::System.Collections.Generic.KeyValuePair this[int index] + { + get => index switch + { + 0 => new global::System.Collections.Generic.KeyValuePair("code", this._code), + 1 => new global::System.Collections.Generic.KeyValuePair("{OriginalFormat}", "M0 {code}"), + + _ => throw new global::System.IndexOutOfRangeException(), // return the same exception LoggerMessage.Define returns in this case + }; + } + + public global::System.Collections.Generic.IEnumerator> GetEnumerator() + { + for (int i = 0; i < 2; i++) + { + yield return this[i]; + } + } + + global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + + /// + /// Message: M0 {code} + /// Level: Trace + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, TCode code) + where TCode : struct, global::System.Enum + { + if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Trace)) + { + logger.Log( + global::Microsoft.Extensions.Logging.LogLevel.Trace, + new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), + new __M0Struct(code), + null, + __M0Struct.Format); + } + } + /// This API supports the logging infrastructure and is not intended to be used directly from your code. It is subject to change in the future. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + private readonly struct __M1Struct : global::System.Collections.Generic.IReadOnlyList> + where T1 : class + where T2 : new() + { + private readonly T1 _value; + private readonly T2 _extra; + + public __M1Struct(T1 value, T2 extra) + { + this._value = value; + this._extra = extra; + + } + + public override string ToString() + { + var value = this._value; + var extra = this._extra; + + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"M1 {value} {extra}"); + } + + public static readonly global::System.Func<__M1Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString(); + + public int Count => 3; + + public global::System.Collections.Generic.KeyValuePair this[int index] + { + get => index switch + { + 0 => new global::System.Collections.Generic.KeyValuePair("value", this._value), + 1 => new global::System.Collections.Generic.KeyValuePair("extra", this._extra), + 2 => new global::System.Collections.Generic.KeyValuePair("{OriginalFormat}", "M1 {value} {extra}"), + + _ => throw new global::System.IndexOutOfRangeException(), // return the same exception LoggerMessage.Define returns in this case + }; + } + + public global::System.Collections.Generic.IEnumerator> GetEnumerator() + { + for (int i = 0; i < 3; i++) + { + yield return this[i]; + } + } + + global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + + /// + /// Message: M1 {value} {extra} + /// Level: Debug + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + public static partial void M1(global::Microsoft.Extensions.Logging.ILogger logger, T1 value, T2 extra) + where T1 : class + where T2 : new() + { + if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Debug)) + { + logger.Log( + global::Microsoft.Extensions.Logging.LogLevel.Debug, + new global::Microsoft.Extensions.Logging.EventId(1, nameof(M1)), + new __M1Struct(value, extra), + null, + __M1Struct.Format); + } + } + /// This API supports the logging infrastructure and is not intended to be used directly from your code. It is subject to change in the future. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + private readonly struct __M2Struct : global::System.Collections.Generic.IReadOnlyList> + { + private readonly T _value; + + public __M2Struct(T value) + { + this._value = value; + + } + + public override string ToString() + { + var value = this._value; + + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"M2 {value}"); + } + + public static readonly global::System.Func<__M2Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString(); + + public int Count => 2; + + public global::System.Collections.Generic.KeyValuePair this[int index] + { + get => index switch + { + 0 => new global::System.Collections.Generic.KeyValuePair("value", this._value), + 1 => new global::System.Collections.Generic.KeyValuePair("{OriginalFormat}", "M2 {value}"), + + _ => throw new global::System.IndexOutOfRangeException(), // return the same exception LoggerMessage.Define returns in this case + }; + } + + public global::System.Collections.Generic.IEnumerator> GetEnumerator() + { + for (int i = 0; i < 2; i++) + { + yield return this[i]; + } + } + + global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + } + + /// + /// Message: M2 {value} + /// Level: Information + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + public static partial void M2(global::Microsoft.Extensions.Logging.ILogger logger, T value) + { + if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information)) + { + logger.Log( + global::Microsoft.Extensions.Logging.LogLevel.Information, + new global::Microsoft.Extensions.Logging.EventId(2, nameof(M2)), + new __M2Struct(value), + null, + __M2Struct.Format); + } + } + } +} \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMoreThan6Params.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMoreThan6Params.generated.txt index 676834aa72248a..6b8af8ad2226f7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMoreThan6Params.generated.txt +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMoreThan6Params.generated.txt @@ -10,15 +10,15 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] private readonly struct __Method9Struct : global::System.Collections.Generic.IReadOnlyList> { - private readonly global::System.Int32 _p1; - private readonly global::System.Int32 _p2; - private readonly global::System.Int32 _p3; - private readonly global::System.Int32 _p4; - private readonly global::System.Int32 _p5; - private readonly global::System.Int32 _p6; - private readonly global::System.Collections.Generic.IEnumerable _p7; - - public __Method9Struct(global::System.Int32 p1, global::System.Int32 p2, global::System.Int32 p3, global::System.Int32 p4, global::System.Int32 p5, global::System.Int32 p6, global::System.Collections.Generic.IEnumerable p7) + private readonly int _p1; + private readonly int _p2; + private readonly int _p3; + private readonly int _p4; + private readonly int _p5; + private readonly int _p6; + private readonly global::System.Collections.Generic.IEnumerable _p7; + + public __Method9Struct(int p1, int p2, int p3, int p4, int p5, int p6, global::System.Collections.Generic.IEnumerable p7) { this._p1 = p1; this._p2 = p2; @@ -80,7 +80,7 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses /// Level: Error /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] - public static partial void Method9(global::Microsoft.Extensions.Logging.ILogger logger, global::System.Int32 p1, global::System.Int32 p2, global::System.Int32 p3, global::System.Int32 p4, global::System.Int32 p5, global::System.Int32 p6, global::System.Collections.Generic.IEnumerable p7) + public static partial void Method9(global::Microsoft.Extensions.Logging.ILogger logger, int p1, int p2, int p3, int p4, int p5, int p6, global::System.Collections.Generic.IEnumerable p7) { if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error)) { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithTwoParams.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithTwoParams.generated.txt index fa142e9fc7aa0a..77313e9f712f35 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithTwoParams.generated.txt +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithTwoParams.generated.txt @@ -6,15 +6,15 @@ namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses partial class TestWithTwoParams { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] - private static readonly global::System.Action, global::System.Exception?> __M0Callback = - global::Microsoft.Extensions.Logging.LoggerMessage.Define>(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0 {a1} {a2}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + private static readonly global::System.Action, global::System.Exception?> __M0Callback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define>(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(0, nameof(M0)), "M0 {a1} {a2}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); /// /// Message: M0 {a1} {a2} /// Level: Error /// [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] - public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, global::System.Int32 a1, global::System.Collections.Generic.IEnumerable a2) + public static partial void M0(global::Microsoft.Extensions.Logging.ILogger logger, int a1, global::System.Collections.Generic.IEnumerable a2) { if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error)) { diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs index 95ea729d1a1d5d..29e9751b437ffa 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs @@ -329,6 +329,27 @@ partial class C Assert.Empty(generatedSourceDiagnostics); } + [Fact] + public async Task TestBaseline_TestWithGenericMethods_Success() + { + string testSourceCode = @" +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + internal static partial class TestWithGenericMethods + { + [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = ""M0 {code}"")] + public static partial void M0(ILogger logger, TCode code) where TCode : struct, System.Enum; + + [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""M1 {value} {extra}"")] + public static partial void M1(ILogger logger, T1 value, T2 extra) where T1 : class where T2 : new(); + + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = ""M2 {value}"")] + public static partial void M2(ILogger logger, T value); + } +}"; + await VerifyAgainstBaselineUsingFile("TestWithGenericMethods.generated.txt", testSourceCode); + } + private async Task VerifyAgainstBaselineUsingFile(string filename, string testSourceCode) { string baseline = LineEndingsHelper.Normalize(File.ReadAllText(Path.Combine("Baselines", filename))); diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs index 83262d8451e028..4ba5800448923d 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorParserTests.cs @@ -825,8 +825,67 @@ partial class C } "); + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MethodGenericWithConstraints() + { + IReadOnlyList diagnostics = await RunGenerator(@" + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = ""Code: {code}"")] + static partial void M1(ILogger logger, TCode code) where TCode : struct, System.Enum; + + [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""Value: {value} Extra: {extra}"")] + static partial void M2(ILogger logger, T1 value, T2 extra) where T1 : class where T2 : new(); + + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = ""Data: {data}"")] + static partial void M3(ILogger logger, T data) where T : unmanaged; + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MethodGenericWithNullableConstraint() + { + IReadOnlyList diagnostics = await RunGenerator(@" + #nullable enable + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""Value: {value}"")] + static partial void M1(ILogger logger, T value) where T : System.IComparable?; + } + "); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MethodGenericWithAllowsRefStructConstraint() + { + IReadOnlyList diagnostics = await RunGenerator(@" + partial class C + { + [LoggerMessage(EventId = 0, Level = LogLevel.Debug, Message = ""{value}"")] + static partial void M1(ILogger logger, T value) where T : allows ref struct; + } + "); + + // AllowsRefLikeType is only available in Roslyn 4.9+ (C# 13). On older Roslyn + // versions the constraint is silently ignored and no diagnostic is produced. + bool roslynSupportsAllowsRefLike = + typeof(Microsoft.CodeAnalysis.ITypeParameterSymbol).GetProperty("AllowsRefLikeType") is not null; + if (!roslynSupportsAllowsRefLike) + { + Assert.Empty(diagnostics); + return; + } + Assert.Single(diagnostics); - Assert.Equal(DiagnosticDescriptors.LoggingMethodIsGeneric.Id, diagnostics[0].Id); + Assert.Equal(DiagnosticDescriptors.LoggingMethodHasAllowsRefStructConstraint.Id, diagnostics[0].Id); } [Theory]