Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
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.IndexOf('{') < 0 ? "" :
_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,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);
Expand Down Expand Up @@ -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 ? "" : " ";
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ITypeParameterSymbol, bool>? s_getAllowsRefLikeType =
(Func<ITypeParameterSymbol, bool>?)
typeof(ITypeParameterSymbol).GetProperty("AllowsRefLikeType")?.GetGetMethod()!.CreateDelegate(typeof(Func<ITypeParameterSymbol, bool>));

private readonly CancellationToken _cancellationToken;
private readonly INamedTypeSymbol _loggerMessageAttribute;
private readonly INamedTypeSymbol _loggerSymbol;
Expand Down Expand Up @@ -239,8 +246,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_getAllowsRefLikeType?.Invoke(tp) == true)
{
Diag(DiagnosticDescriptors.LoggingMethodHasAllowsRefStructConstraint, method.Identifier.GetLocation());
keepMethod = false;
break;
}
}

bool success = ExtractTemplates(message, lm.TemplateMap, lm.TemplateList);
if (!success)
{
Expand All @@ -263,13 +291,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 @@ -369,6 +390,7 @@ public IReadOnlyList<LoggerClass> GetLogClasses(IEnumerable<ClassDeclarationSynt

string typeName = paramTypeSymbol.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions |
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));

var lp = new LoggerParameter
Expand Down Expand Up @@ -714,6 +736,48 @@ 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)
{
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)
Expand Down Expand Up @@ -918,6 +982,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 +1000,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 +1023,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 +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 &&
Expand All @@ -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());
Expand Down Expand Up @@ -1102,6 +1171,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
Loading
Loading