Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using Microsoft.CodeAnalysis;
Expand Down Expand Up @@ -60,6 +59,7 @@ public string Emit(IReadOnlyList<LoggerClass> 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
Expand Down Expand Up @@ -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<global::System.Collections.Generic.KeyValuePair<string, object?>>
{nestedIndentation}private readonly struct __{lm.UniqueName}Struct");
GenTypeParameterList(lm);
_builder.Append($" : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>");
GenTypeConstraints(lm, nestedIndentation + " ");
_builder.AppendLine($@"
{nestedIndentation}{{");
GenFields(lm, nestedIndentation);

Expand All @@ -175,7 +179,7 @@ private void GenStruct(LoggerMethod lm, string nestedIndentation)
GenVariableAssignments(lm, nestedIndentation);

string formatMethodBegin =
!lm.Message.Contains('{') ? "" :
!lm.Message.Contains("{") ? "" :
Comment thread
tarekgh marked this conversation as resolved.
Outdated
_hasStringCreate ? "string.Create(global::System.Globalization.CultureInfo.InvariantCulture, " :
"global::System.FormattableString.Invariant(";
string formatMethodEnd = formatMethodBegin.Length > 0 ? ")" : "";
Expand All @@ -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};

Expand Down Expand Up @@ -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])
Expand All @@ -385,6 +391,43 @@ 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($"\n {nestedIndentation}where {tp.Name} : {tp.Constraints}");
Comment thread
svick marked this conversation as resolved.
Outdated
}
}
}

private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
{
string level = GetLogLevel(lm);
Expand Down Expand Up @@ -414,11 +457,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 ? "" : " ";
Expand Down Expand Up @@ -448,7 +495,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ internal sealed class Parser
{
internal const string LoggerMessageAttribute = "Microsoft.Extensions.Logging.LoggerMessageAttribute";

// ITypeParameterSymbol.AllowsRefLikeType was added in Roslyn 4.9 (C# 13). Access via reflection
// so the same source file compiles against all supported Roslyn versions.
private static readonly System.Reflection.PropertyInfo? s_allowsRefLikeTypeProperty =
typeof(ITypeParameterSymbol).GetProperty("AllowsRefLikeType");
Comment thread
svick marked this conversation as resolved.
Outdated

private readonly CancellationToken _cancellationToken;
private readonly INamedTypeSymbol _loggerMessageAttribute;
private readonly INamedTypeSymbol _loggerSymbol;
Expand Down Expand Up @@ -239,8 +244,29 @@ 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

// Forbid 'allows ref struct': the code generator stores template parameters as
// fields in a generated struct, so ref-struct type arguments cannot be supported.
foreach (ITypeParameterSymbol tp in logMethodSymbol.TypeParameters)
{
if (s_allowsRefLikeTypeProperty?.GetValue(tp) is true)
{
Diag(DiagnosticDescriptors.LoggingMethodIsGeneric, method.Identifier.GetLocation());
keepMethod = false;
break;
}
}

bool success = ExtractTemplates(message, lm.TemplateMap, lm.TemplateList);
if (!success)
{
Expand All @@ -263,13 +289,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 +733,43 @@ 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.
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions |
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 +974,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 +992,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 +1015,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 +1035,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 +1054,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 +1163,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 @@ -153,7 +153,7 @@
<value>Logging methods must be partial</value>
</data>
<data name="LoggingMethodIsGenericMessage" xml:space="preserve">
<value>Logging methods cannot be generic</value>
<value>Logging methods cannot have type parameters with 'allows ref struct' constraints</value>
</data>
<data name="ShouldntMentionInTemplateMessage" xml:space="preserve">
<value>Don't include a template for {0} in the logging message since it is implicitly taken care of</value>
Expand Down
Loading
Loading