Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 @@ -59,6 +59,12 @@ public string Emit(IReadOnlyList<LoggerClass> logClasses, CancellationToken canc

private static bool UseLoggerMessageDefine(LoggerMethod lm)
{
if (lm.TypeParameters.Count > 0)
{
// Generic methods require per-call type argument instantiation; LoggerMessage.Define requires a static callback.
return false;
}
Comment thread
svick marked this conversation as resolved.
Outdated

bool result =
(lm.TemplateParameters.Count <= MaxLoggerMessageDefineArguments) && // more args than LoggerMessage.Define can handle
(lm.Level != null) && // dynamic log level, which LoggerMessage.Define can't handle
Expand Down Expand Up @@ -146,11 +152,13 @@ namespace {lc.Namespace}

private void GenStruct(LoggerMethod lm, string nestedIndentation)
{
string typeParamList = GetTypeParameterList(lm);
string typeConstraints = GetTypeConstraints(lm, nestedIndentation + " ");
_builder.AppendLine($@"
{nestedIndentation}/// {GeneratedTypeSummary}
{nestedIndentation}[{s_generatedCodeAttribute}]
{nestedIndentation}[{EditorBrowsableAttribute}]
{nestedIndentation}private readonly struct __{lm.UniqueName}Struct : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>
{nestedIndentation}private readonly struct __{lm.UniqueName}Struct{typeParamList} : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>{typeConstraints}
{nestedIndentation}{{");
GenFields(lm, nestedIndentation);

Expand Down Expand Up @@ -185,7 +193,7 @@ 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{typeParamList}, global::System.Exception?, string> Format = (state, ex) => state.ToString();

{nestedIndentation}public int Count => {lm.TemplateParameters.Count + 1};

Expand Down Expand Up @@ -369,7 +377,7 @@ private void GenArguments(LoggerMethod lm)

private void GenHolder(LoggerMethod lm)
{
string typeName = $"__{lm.UniqueName}Struct";
string typeName = $"__{lm.UniqueName}Struct{GetTypeParameterList(lm)}";
Comment thread
svick marked this conversation as resolved.
Outdated

_builder.Append($"new {typeName}(");
foreach (LoggerParameter p in lm.TemplateParameters)
Expand All @@ -385,6 +393,35 @@ private void GenHolder(LoggerMethod lm)
_builder.Append(')');
}

private static string GetTypeParameterList(LoggerMethod lm)
{
if (lm.TypeParameters.Count == 0)
{
return string.Empty;
}

return "<" + string.Join(", ", lm.TypeParameters.Select(tp => tp.Name)) + ">";
}

private static string GetTypeConstraints(LoggerMethod lm, string nestedIndentation)
{
if (lm.TypeParameters.Count == 0)
{
return string.Empty;
}

var sb = new StringBuilder();
foreach (LoggerMethodTypeParameter tp in lm.TypeParameters)
{
if (tp.Constraints is not null)
{
sb.Append($"\n {nestedIndentation}where {tp.Name} : {tp.Constraints}");
}
}

return sb.ToString();
}
Comment thread
svick marked this conversation as resolved.
Outdated

private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
{
string level = GetLogLevel(lm);
Expand Down Expand Up @@ -412,13 +449,21 @@ private void GenLogMethod(LoggerMethod lm, string nestedIndentation)

GenMethodDocumentation(lm, nestedIndentation);

string typeParamList = GetTypeParameterList(lm);
string typeConstraints = GetTypeConstraints(lm, nestedIndentation);

_builder.Append($@"
{nestedIndentation}[{s_generatedCodeAttribute}]
{nestedIndentation}{lm.Modifiers} void {lm.Name}({extension}");
{nestedIndentation}{lm.Modifiers} void {lm.Name}{typeParamList}({extension}");

GenParameters(lm);

_builder.Append($@")
_builder.Append(')');
if (typeConstraints.Length > 0)
{
_builder.Append(typeConstraints);
}
_builder.Append($@"
{nestedIndentation}{{");

string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : " ";
Expand Down Expand Up @@ -448,7 +493,7 @@ 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{typeParamList}.Format);");
}

if (!lm.SkipEnabledCheck)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSynt
SkipEnabledCheck = skipEnabledCheck
};

foreach (ITypeParameterSymbol tp in logMethodSymbol.TypeParameters)
{
lm.TypeParameters.Add(new LoggerMethodTypeParameter
{
Name = tp.Name,
Constraints = GetTypeParameterConstraints(tp)
});
}

bool keepMethod = true; // whether or not we want to keep the method definition or if it's got errors making it so we should discard it instead

bool success = ExtractTemplates(message, lm.TemplateMap, lm.TemplateList);
Expand All @@ -263,13 +272,6 @@ public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSynt
keepMethod = false;
}

if (method.Arity > 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)
Expand Down Expand Up @@ -714,6 +716,42 @@ private static string GenerateClassName(TypeDeclarationSyntax typeDeclaration)
return (loggerField, 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 (ITypeSymbol constraintType in typeParameter.ConstraintTypes)
{
constraints.Add(constraintType.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
Comment thread
svick marked this conversation as resolved.
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)));
}
Comment thread
svick marked this conversation as resolved.

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)
Expand Down Expand Up @@ -918,6 +956,7 @@ internal sealed class LoggerMethod
public readonly List<LoggerParameter> TemplateParameters = new();
public readonly Dictionary<string, string> TemplateMap = new(StringComparer.OrdinalIgnoreCase);
public readonly List<string> TemplateList = new();
public readonly List<LoggerMethodTypeParameter> TypeParameters = new();
public string Name = string.Empty;
public string UniqueName = string.Empty;
public string Message = string.Empty;
Expand All @@ -935,6 +974,7 @@ internal sealed class LoggerMethod
TemplateParameters = TemplateParameters.Select(p => p.ToSpec()).ToImmutableEquatableArray(),
TemplateMap = TemplateMap.Select(kvp => new KeyValuePairEquatable<string, string>(kvp.Key, kvp.Value)).ToImmutableEquatableArray(),
TemplateList = TemplateList.ToImmutableEquatableArray(),
TypeParameters = TypeParameters.Select(tp => tp.ToSpec()).ToImmutableEquatableArray(),
Name = Name,
UniqueName = UniqueName,
Message = Message,
Expand All @@ -957,6 +997,7 @@ internal sealed record LoggerMethodSpec : IEquatable<LoggerMethodSpec>
public required ImmutableEquatableArray<LoggerParameterSpec> TemplateParameters { get; init; }
public required ImmutableEquatableArray<KeyValuePairEquatable<string, string>> TemplateMap { get; init; }
public required ImmutableEquatableArray<string> TemplateList { get; init; }
public required ImmutableEquatableArray<LoggerMethodTypeParameterSpec> TypeParameters { get; init; }
public required string Name { get; init; }
public required string UniqueName { get; init; }
public required string Message { get; init; }
Expand All @@ -976,6 +1017,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 &&
Expand All @@ -994,6 +1036,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());
Expand Down Expand Up @@ -1102,6 +1145,44 @@ public override int GetHashCode()
}
}

/// <summary>
/// A type parameter of a generic logging method.
/// </summary>
internal sealed class LoggerMethodTypeParameter
{
public string Name = string.Empty;
public string? Constraints;

public LoggerMethodTypeParameterSpec ToSpec() => new LoggerMethodTypeParameterSpec
{
Name = Name,
Constraints = Constraints
};
}

/// <summary>
/// Immutable specification of a type parameter for incremental caching.
/// </summary>
internal sealed record LoggerMethodTypeParameterSpec : IEquatable<LoggerMethodTypeParameterSpec>
{
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;
}
}

/// <summary>
/// Returns a non-randomized hash code for the given string.
/// We always return a positive value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<TCode>(ILogger logger, TCode code) where TCode : struct, System.Enum;

[LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""M1 {value} {extra}"")]
public static partial void M1<T1, T2>(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<T>(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)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,8 +825,24 @@ partial class C
}
");

Assert.Single(diagnostics);
Assert.Equal(DiagnosticDescriptors.LoggingMethodIsGeneric.Id, diagnostics[0].Id);
Assert.Empty(diagnostics);
}

[Fact]
public async Task MethodGenericWithConstraints()
{
IReadOnlyList<Diagnostic> diagnostics = await RunGenerator(@"
partial class C
{
[LoggerMessage(EventId = 0, Level = LogLevel.Trace, Message = ""Code: {code}"")]
static partial void M1<TCode>(ILogger logger, TCode code) where TCode : struct, System.Enum;

[LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""Value: {value}"")]
static partial void M2<T1, T2>(ILogger logger, T1 value, T2 extra) where T1 : class where T2 : new();
}
");

Assert.Empty(diagnostics);
}

[Theory]
Expand Down
Loading