Work on the logging generator. (#4840)

- The generator now produces a warning when asked to log an object
which doesn't implement ToString(), IConvertible, or IFormattable.
Fixed #4835.

- Added support for the Transitive property in the LogProperties attribute. When
set to true, this causes automatic transitive traversal of a complex object, instead
of requiring manual annotations of individual properties. Fixes #4738.

- Introduce the [TagName] attribute to make it possible to control the tag name
used when logging a parameter or property. Fixes #4576.

- Fixed some situations where unnatural errors were produced as a
result of a prior error. The dummy follow-on errors are now avoided.

- Fixed handling of cases where parameters or properties were of type of the
non-generic IEnumerable. The specific type wasn't being recorgnized and treated
as an enumerable.

Co-authored-by: Martin Taillefer <mataille@microsoft.com>
This commit is contained in:
Martin Taillefer 2023-12-27 11:22:32 -08:00 коммит произвёл GitHub
Родитель 40fdaf9b31
Коммит f4315cd121
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
34 изменённых файлов: 566 добавлений и 254 удалений

Просмотреть файл

@ -8,56 +8,6 @@
| `CTXOPTGEN002` | The options context type does not have usable properties |
| `CTXOPTGEN003` | The options context cannot be a ref-like type |
# Design
| Diagnostic ID | Description |
| :---------------- | :---------- |
| `AUTOCLIENTGEN001` | API client interfaces must not be nested types |
| `AUTOCLIENTGEN002` | REST API client does not have methods defined |
| `AUTOCLIENTGEN003` | An API method must not contain more than one REST method attribute |
| `AUTOCLIENTGEN004` | Invalid API method return type |
| `AUTOCLIENTGEN005` | API methods can't be generic |
| `AUTOCLIENTGEN006` | The current HTTP method does not support the body tag |
| `AUTOCLIENTGEN007` | API methods must not be static |
| `AUTOCLIENTGEN008` | HTTP method missing |
| `AUTOCLIENTGEN009` | The API interface cannot be generic |
| `AUTOCLIENTGEN010` | Invalid API interface name |
| `AUTOCLIENTGEN011` | Duplicate body attribute |
| `AUTOCLIENTGEN012` | URL parameter missing from path |
| `AUTOCLIENTGEN013` | REST API method has more than one cancellation token |
| `AUTOCLIENTGEN014` | Missing CancellationToken from REST API method |
| `AUTOCLIENTGEN015` | API method path should not contain query |
| `AUTOCLIENTGEN016` | A REST API method's request name must be unique |
| `AUTOCLIENTGEN017` | Invalid HttpClient name |
| `AUTOCLIENTGEN018` | Invalid dependency name |
| `AUTOCLIENTGEN019` | Invalid header name |
| `AUTOCLIENTGEN020` | Invalid header value |
| `AUTOCLIENTGEN021` | Invalid REST method path |
| `AUTOCLIENTGEN022` | Invalid request name |
# ExtraAnalyzers
| Diagnostic ID | Category | Description |
| :---------------- | :---------- | :---------- |
| `EA0000` | Performance | Use source generated logging methods for improved performance |
| `EA0001` | Performance | Perform message formatting in the body of the logging method |
| `EA0002` | Reliability | Use 'System.TimeProvider' to make the code easier to test |
| `EA0003` | Performance | Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith' |
| `EA0004` | Performance | Make types declared in an executable internal |
| `EA0005` | Performance | Consider using an array instead of a collection |
| `EA0006` | Performance | Replace uses of 'Enum.GetName' and 'Enum.ToString' for improved performance |
| `EA0007` | Performance | Use 'System.ValueTuple' instead of 'System.Tuple' for improved performance |
| `EA0008` | Performance | Use generic collections instead of legacy collections for improved performance |
| `EA0009` | Performance | Use 'System.MemoryExtensions.Split' for improved performance |
| `EA0010` | Correctness | Fire-and-forget async call inside a 'using' block |
| `EA0011` | Performance | Consider removing unnecessary conditional access operator (?) |
| `EA0012` | Performance | Consider removing unnecessary null coalescing assignment (??=) |
| `EA0013` | Performance | Consider removing unnecessary null coalescing operator (??) |
| `EA0014` | Resilience | The async method doesn't support cancellation |
# Experiments
As new functionality is introduced to this repo, new in-development APIs are marked as being experimental. Experimental APIs offer no
@ -72,14 +22,12 @@ If you use experimental APIs, you will get one of the diagnostic shown below. Th
using such an API so that you can avoid accidentally depending on experimental features. You may suppress these diagnostics
if desired.
| Diagnostic ID | Description |
| :---------------- | :---------- |
| `EXTEXP0001` | Resilience experiments |
| `EXTEXP0002` | Compliance experiments |
| `EXTEXP0003` | Telemetry experiments |
| `EXTEXP0004` | TimeProvider experiments |
| `EXTEXP0005` | AutoClient experiments |
| `EXTEXP0006` | AsyncState experiments |
| `EXTEXP0007` | Health check experiments |
| `EXTEXP0008` | Resource monitoring experiments |
@ -134,7 +82,7 @@ if desired.
| `LOGGEN033` | Method parameter can't be used with a tag provider |
| `LOGGEN034` | Attribute can't be used in this context |
| `LOGGEN035` | The logging method parameter leaks sensitive data |
| `LOGGEN036` | A value being logged doesn't have an effective way to be converted into a string |
# Metrics

Просмотреть файл

@ -74,10 +74,10 @@ internal sealed partial class Emitter : EmitterBase
OutLn();
}
var stateName = PickUniqueName("state", lm.Parameters.Select(p => p.Name));
var stateName = PickUniqueName("state", lm.Parameters.Select(p => p.ParameterName));
OutLn($"var {stateName} = {LoggerMessageHelperType}.ThreadLocalState;");
GenPropertyLoads(lm, stateName, out int numReservedUnclassifiedTags, out int numReservedClassifiedTags);
GenTagWrites(lm, stateName, out int numReservedUnclassifiedTags, out int numReservedClassifiedTags);
OutLn();
OutLn($"{logger}.Log(");
@ -97,7 +97,7 @@ internal sealed partial class Emitter : EmitterBase
OutLn($"{stateName},");
OutLn($"{exceptionArg},");
var lambdaStateName = PickUniqueName("s", lm.TemplateToParameterName.Select(kvp => kvp.Key));
var lambdaStateName = PickUniqueName("s", lm.Templates);
OutLn($"[{GeneratorUtilities.GeneratedCodeAttribute}] static string ({lambdaStateName}, {exceptionLambdaName}) =>");
OutOpenBrace();
@ -158,7 +158,7 @@ internal sealed partial class Emitter : EmitterBase
if (p.ImplementsISpanFormattable)
{
// pass object as it, it will be formatted directly into the output buffer
// pass object as is, it will be formatted directly into the output buffer
return false;
}
@ -211,11 +211,11 @@ internal sealed partial class Emitter : EmitterBase
{
if (p.IsException)
{
exceptionArg = p.Name;
exceptionArg = p.ParameterName;
if (p.UsedAsTemplate)
{
exceptionLambdaArg = lm.GetParameterNameInTemplate(p);
exceptionLambdaArg = lm.GetTemplatesForParameter(p)[0];
}
break;
@ -235,11 +235,11 @@ internal sealed partial class Emitter : EmitterBase
return p.IsNormalParameter && !p.HasTagProvider && !p.HasProperties;
}
void GenPropertyLoads(LoggingMethod lm, string stateName, out int numReservedUnclassifiedTags, out int numReservedClassifiedTags)
void GenTagWrites(LoggingMethod lm, string stateName, out int numReservedUnclassifiedTags, out int numReservedClassifiedTags)
{
int numUnclassifiedTags = 0;
int numClassifiedTags = 0;
var tmpVarName = PickUniqueName("tmp", lm.Parameters.Select(p => p.Name));
var tmpVarName = PickUniqueName("tmp", lm.Parameters.Select(p => p.ParameterName));
foreach (var p in lm.Parameters)
{
@ -288,20 +288,20 @@ internal sealed partial class Emitter : EmitterBase
{
if (NeedsASlot(p) && !p.HasDataClassification)
{
var key = $"\"{lm.GetParameterNameInTemplate(p)}\"";
var key = $"\"{p.TagName}\"";
string value;
if (p.IsEnumerable)
{
value = p.PotentiallyNull
? $"{p.NameWithAt} != null ? {LoggerMessageHelperType}.Stringify({p.NameWithAt}) : null"
: $"{LoggerMessageHelperType}.Stringify({p.NameWithAt})";
? $"{p.ParameterNameWithAt} != null ? {LoggerMessageHelperType}.Stringify({p.ParameterNameWithAt}) : null"
: $"{LoggerMessageHelperType}.Stringify({p.ParameterNameWithAt})";
}
else
{
value = ShouldStringifyParameter(p)
? ConvertParameterToString(p, p.NameWithAt)
: p.NameWithAt;
? ConvertParameterToString(p, p.ParameterNameWithAt)
: p.ParameterNameWithAt;
}
OutLn($"{stateName}.TagArray[{--count}] = new({key}, {value});");
@ -348,12 +348,12 @@ internal sealed partial class Emitter : EmitterBase
{
if (NeedsASlot(p) && p.HasDataClassification)
{
var key = $"\"{lm.GetParameterNameInTemplate(p)}\"";
var key = $"\"{p.TagName}\"";
var classification = MakeClassificationValue(p.ClassificationAttributeTypes);
var value = ShouldStringifyParameter(p)
? ConvertParameterToString(p, p.NameWithAt)
: p.NameWithAt;
? ConvertParameterToString(p, p.ParameterNameWithAt)
: p.ParameterNameWithAt;
OutLn($"{stateName}.ClassifiedTagArray[{--count}] = new({key}, {value}, {classification});");
}
@ -469,10 +469,10 @@ internal sealed partial class Emitter : EmitterBase
}
else
{
OutLn($"{stateName}.TagNamePrefix = nameof({p.NameWithAt});");
OutLn($"{stateName}.TagNamePrefix = nameof({p.ParameterNameWithAt});");
}
OutLn($"{p.TagProvider!.ContainingType}.{p.TagProvider.MethodName}({stateName}, {p.NameWithAt});");
OutLn($"{p.TagProvider!.ContainingType}.{p.TagProvider.MethodName}({stateName}, {p.ParameterNameWithAt});");
}
}
}
@ -488,20 +488,22 @@ internal sealed partial class Emitter : EmitterBase
{
if (p.UsedAsTemplate)
{
var key = lm.GetParameterNameInTemplate(p);
var atSign = p.NeedsAtSign ? "@" : string.Empty;
if (p.PotentiallyNull)
var templates = lm.GetTemplatesForParameter(p);
foreach (var t in templates)
{
const string Null = "\"(null)\"";
OutLn($"var {atSign}{key} = {lambdaStateName}.TagArray[{index}].Value ?? {Null};");
}
else
{
OutLn($"var {atSign}{key} = {lambdaStateName}.TagArray[{index}].Value;");
}
var atSign = p.NeedsAtSign ? "@" : string.Empty;
if (p.PotentiallyNull)
{
const string Null = "\"(null)\"";
OutLn($"var {atSign}{t} = {lambdaStateName}.TagArray[{index}].Value ?? {Null};");
}
else
{
OutLn($"var {atSign}{t} = {lambdaStateName}.TagArray[{index}].Value;");
}
generatedAssignments = true;
generatedAssignments = true;
}
}
index--;
@ -515,20 +517,22 @@ internal sealed partial class Emitter : EmitterBase
{
if (p.UsedAsTemplate)
{
var key = lm.GetParameterNameInTemplate(p);
var atSign = p.NeedsAtSign ? "@" : string.Empty;
if (p.PotentiallyNull)
var templates = lm.GetTemplatesForParameter(p);
foreach (var t in templates)
{
const string Null = "\"(null)\"";
OutLn($"var {atSign}{key} = {lambdaStateName}.RedactedTagArray[{index}].Value ?? {Null};");
}
else
{
OutLn($"var {atSign}{key} = {lambdaStateName}.RedactedTagArray[{index}].Value;");
}
var atSign = p.NeedsAtSign ? "@" : string.Empty;
if (p.PotentiallyNull)
{
const string Null = "\"(null)\"";
OutLn($"var {atSign}{t} = {lambdaStateName}.RedactedTagArray[{index}].Value ?? {Null};");
}
else
{
OutLn($"var {atSign}{t} = {lambdaStateName}.RedactedTagArray[{index}].Value;");
}
generatedAssignments = true;
generatedAssignments = true;
}
}
index--;
@ -547,7 +551,7 @@ internal sealed partial class Emitter : EmitterBase
{
if (p.IsLogger)
{
logger = p.Name;
logger = p.ParameterName;
isNullable = p.IsNullable;
break;
}
@ -597,7 +601,7 @@ internal sealed partial class Emitter : EmitterBase
_ = stringBuilder.Append(template);
}
_ = stringBuilder.Replace(item.Name, item.NameWithAt);
_ = stringBuilder.Replace(item.ParameterName, item.ParameterNameWithAt);
}
var result = stringBuilder is null
@ -618,10 +622,10 @@ internal sealed partial class Emitter : EmitterBase
{
if (p.Qualifier != null)
{
return $"{p.Qualifier} {p.Type} {p.NameWithAt}";
return $"{p.Qualifier} {p.Type} {p.ParameterNameWithAt}";
}
return $"{p.Type} {p.NameWithAt}";
return $"{p.Type} {p.ParameterNameWithAt}";
}));
}
@ -647,12 +651,12 @@ internal sealed partial class Emitter : EmitterBase
}
_ = localStringBuilder
.Append(needAts ? property.NameWithAt : property.Name)
.Append(needAts ? property.PropertyNameWithAt : property.PropertyName)
.Append(property.PotentiallyNull ? separator : adjustedNonNullSeparator);
}
// Last item:
_ = localStringBuilder.Append(needAts ? leafProperty.NameWithAt : leafProperty.Name);
_ = localStringBuilder.Append(needAts ? leafProperty.PropertyNameWithAt : leafProperty.PropertyName);
return localStringBuilder.ToString();
}

Просмотреть файл

@ -55,7 +55,7 @@ internal sealed partial class Emitter : EmitterBase
{
if (p.IsLogLevel)
{
level = p.Name;
level = p.ParameterName;
break;
}
}

Просмотреть файл

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Microsoft.Gen.Logging.Model;
@ -14,7 +15,7 @@ namespace Microsoft.Gen.Logging.Model;
internal sealed class LoggingMethod
{
public readonly List<LoggingMethodParameter> Parameters = [];
public readonly Dictionary<string, string> TemplateToParameterName = new(StringComparer.OrdinalIgnoreCase);
public readonly List<string> Templates = [];
public string Name = string.Empty;
public string Message = string.Empty;
public int? Level;
@ -28,8 +29,33 @@ internal sealed class LoggingMethod
public bool LoggerMemberNullable;
public bool HasXmlDocumentation;
public string GetParameterNameInTemplate(LoggingMethodParameter parameter)
=> TemplateToParameterName.TryGetValue(parameter.Name, out var value)
? value
: parameter.Name;
public LoggingMethodParameter? GetParameterForTemplate(string templateName)
{
foreach (var p in Parameters)
{
if (templateName.Equals(p.ParameterName, StringComparison.OrdinalIgnoreCase))
{
return p;
}
}
return null;
}
public List<string> GetTemplatesForParameter(LoggingMethodParameter lp)
=> GetTemplatesForParameter(lp.ParameterName);
public List<string> GetTemplatesForParameter(string parameterName)
{
HashSet<string> templates = [];
foreach (var t in Templates)
{
if (parameterName.Equals(t, StringComparison.OrdinalIgnoreCase))
{
_ = templates.Add(t);
}
}
return templates.ToList();
}
}

Просмотреть файл

@ -13,7 +13,8 @@ namespace Microsoft.Gen.Logging.Model;
[DebuggerDisplay("{Name}")]
internal sealed class LoggingMethodParameter
{
public string Name = string.Empty;
public string ParameterName = string.Empty;
public string TagName = string.Empty;
public string Type = string.Empty;
public string? Qualifier;
public bool NeedsAtSign;
@ -26,6 +27,7 @@ internal sealed class LoggingMethodParameter
public bool ImplementsIConvertible;
public bool ImplementsIFormattable;
public bool ImplementsISpanFormattable;
public bool HasCustomToString;
public bool SkipNullProperties;
public bool OmitReferenceName;
public bool UsedAsTemplate;
@ -33,7 +35,7 @@ internal sealed class LoggingMethodParameter
public List<LoggingProperty> Properties = [];
public TagProvider? TagProvider;
public string NameWithAt => NeedsAtSign ? "@" + Name : Name;
public string ParameterNameWithAt => NeedsAtSign ? "@" + ParameterName : ParameterName;
public string PotentiallyNullableType
=> (IsReference && !IsNullable)
@ -48,4 +50,5 @@ internal sealed class LoggingMethodParameter
public bool HasProperties => Properties.Count > 0;
public bool HasTagProvider => TagProvider is not null;
public bool PotentiallyNull => (IsReference && !IsLogger) || IsNullable;
public bool IsStringifiable => HasCustomToString || ImplementsIConvertible || ImplementsIFormattable || IsEnumerable;
}

Просмотреть файл

@ -16,7 +16,8 @@ internal static class LoggingMethodParameterExtensions
var firstProperty = new LoggingProperty
{
Name = parameter.Name,
PropertyName = parameter.ParameterName,
TagName = parameter.TagName,
NeedsAtSign = parameter.NeedsAtSign,
Type = parameter.Type,
IsNullable = parameter.IsNullable,

Просмотреть файл

@ -9,7 +9,8 @@ namespace Microsoft.Gen.Logging.Model;
[DebuggerDisplay("{Name}")]
internal sealed class LoggingProperty
{
public string Name = string.Empty;
public string PropertyName = string.Empty;
public string TagName = string.Empty;
public string Type = string.Empty;
public HashSet<string> ClassificationAttributeTypes = [];
public bool NeedsAtSign;
@ -19,6 +20,7 @@ internal sealed class LoggingProperty
public bool ImplementsIConvertible;
public bool ImplementsIFormattable;
public bool ImplementsISpanFormattable;
public bool HasCustomToString;
public List<LoggingProperty> Properties = [];
public bool OmitReferenceName;
public TagProvider? TagProvider;
@ -26,6 +28,7 @@ internal sealed class LoggingProperty
public bool HasDataClassification => ClassificationAttributeTypes.Count > 0;
public bool HasProperties => Properties.Count > 0;
public bool HasTagProvider => TagProvider is not null;
public string NameWithAt => NeedsAtSign ? "@" + Name : Name;
public string PropertyNameWithAt => NeedsAtSign ? "@" + PropertyName : PropertyName;
public bool PotentiallyNull => IsReference || IsNullable;
public bool IsStringifiable => HasCustomToString || ImplementsIConvertible || ImplementsIFormattable || IsEnumerable;
}

Просмотреть файл

@ -15,6 +15,7 @@ internal static class AttributeProcessors
private const string SkipEnabledCheckProperty = "SkipEnabledCheck";
private const string SkipNullProperties = "SkipNullProperties";
private const string OmitReferenceName = "OmitReferenceName";
private const string Transitive = "Transitive";
private const int LogLevelError = 4;
private const int LogLevelCritical = 5;
@ -124,10 +125,11 @@ internal static class AttributeProcessors
return (eventId, level, message, eventName, skipEnabledCheck);
}
public static (bool skipNullProperties, bool omitReferenceName) ExtractLogPropertiesAttributeValues(AttributeData attr)
public static (bool skipNullProperties, bool omitReferenceName, bool transitive) ExtractLogPropertiesAttributeValues(AttributeData attr)
{
bool skipNullProperties = false;
bool omitReferenceName = false;
bool transitive = false;
foreach (var a in attr.NamedArguments)
{
@ -148,10 +150,17 @@ internal static class AttributeProcessors
omitReferenceName = b;
}
}
else if (a.Key == Transitive)
{
if (v is bool b)
{
transitive = b;
}
}
}
}
return (skipNullProperties, omitReferenceName);
return (skipNullProperties, omitReferenceName, transitive);
}
public static (bool omitReferenceName, ITypeSymbol providerType, string providerMethodName) ExtractTagProviderAttributeValues(AttributeData attr)
@ -180,4 +189,7 @@ internal static class AttributeProcessors
return (omitReferenceName, providerType!, providerMethodName!);
}
public static string ExtractTagNameAttributeValues(AttributeData attr)
=> attr.ConstructorArguments[0].Value as string ?? string.Empty;
}

Просмотреть файл

@ -196,10 +196,10 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase
messageFormat: Resources.LogPropertiesHiddenPropertyDetectedMessage,
category: Category);
public static DiagnosticDescriptor LogPropertiesNameCollision { get; } = Make(
public static DiagnosticDescriptor TagNameCollision { get; } = Make(
id: DiagnosticIds.LoggerMessage.LOGGEN029,
title: Resources.LogPropertiesNameCollisionTitle,
messageFormat: Resources.LogPropertiesNameCollisionMessage,
title: Resources.TagNameCollisionTitle,
messageFormat: Resources.TagNameCollisionMessage,
category: Category);
public static DiagnosticDescriptor EmptyLoggingMethod { get; } = Make(
@ -237,4 +237,11 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase
title: Resources.RecordTypeSensitiveArgumentIsInTemplateTitle,
messageFormat: Resources.RecordTypeSensitiveArgumentIsInTemplateMessage,
category: Category);
public static DiagnosticDescriptor DefaultToString { get; } = Make(
id: DiagnosticIds.LoggerMessage.LOGGEN036,
title: Resources.DefaultToStringTitle,
messageFormat: Resources.DefaultToStringMessage,
category: Category,
DiagnosticSeverity.Warning);
}

Просмотреть файл

@ -47,13 +47,13 @@ internal partial class Parser
paramTypeSymbol = ((INamedTypeSymbol)paramTypeSymbol).TypeArguments[0];
}
(lp.SkipNullProperties, lp.OmitReferenceName) = AttributeProcessors.ExtractLogPropertiesAttributeValues(logPropertiesAttribute);
(lp.SkipNullProperties, lp.OmitReferenceName, bool transitive) = AttributeProcessors.ExtractLogPropertiesAttributeValues(logPropertiesAttribute);
var typesChain = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
_ = typesChain.Add(paramTypeSymbol); // Add itself
var props = GetTypePropertiesToLog(paramTypeSymbol, typesChain, symbols, ref foundDataClassificationAttributes);
var props = GetTypePropertiesToLog(paramTypeSymbol, typesChain, symbols, transitive, ref foundDataClassificationAttributes);
if (props == null)
{
return false;
@ -86,6 +86,7 @@ internal partial class Parser
ITypeSymbol type,
ISet<ITypeSymbol> typesChain,
SymbolHolder symbols,
bool transitive,
ref bool foundDataClassificationAttributes)
{
var result = new List<LoggingProperty>();
@ -182,9 +183,15 @@ internal partial class Parser
extractedType = ((INamedTypeSymbol)extractedType).TypeArguments[0];
}
var tagNameAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.TagNameAttribute, property);
var tagName = tagNameAttribute != null
? AttributeProcessors.ExtractTagNameAttributeValues(tagNameAttribute)
: property.Name;
var lp = new LoggingProperty
{
Name = property.Name,
PropertyName = property.Name,
TagName = tagName,
Type = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
ClassificationAttributeTypes = classification,
IsReference = property.Type.IsReferenceType,
@ -193,6 +200,7 @@ internal partial class Parser
ImplementsIConvertible = property.Type.ImplementsIConvertible(symbols),
ImplementsIFormattable = property.Type.ImplementsIFormattable(symbols),
ImplementsISpanFormattable = property.Type.ImplementsISpanFormattable(symbols),
HasCustomToString = property.Type.HasCustomToString(),
};
if (!property.DeclaringSyntaxReferences.IsDefaultOrEmpty)
@ -233,14 +241,16 @@ internal partial class Parser
return null;
}
if (logPropertiesAttribute != null)
if (logPropertiesAttribute != null || (transitive && tagProviderAttribute == null && logPropertyIgnoreAttribute == null))
{
_ = CanLogProperties(property, property.Type, symbols);
if ((property.DeclaredAccessibility != Accessibility.Public || property.IsStatic)
|| (property.GetMethod == null || property.GetMethod.DeclaredAccessibility != Accessibility.Public))
{
Diag(DiagDescriptors.InvalidAttributeUsage, logPropertiesAttribute.ApplicationSyntaxReference?.GetSyntax(_cancellationToken).GetLocation(), "LogProperties");
if (logPropertiesAttribute != null)
{
Diag(DiagDescriptors.InvalidAttributeUsage, logPropertiesAttribute.ApplicationSyntaxReference?.GetSyntax(_cancellationToken).GetLocation(), "LogProperties");
}
continue;
}
@ -260,13 +270,16 @@ internal partial class Parser
extractedType = ((INamedTypeSymbol)extractedType).TypeArguments[0];
}
_ = typesChain.Add(namedType);
var props = GetTypePropertiesToLog(extractedType, typesChain, symbols, ref foundDataClassificationAttributes);
_ = typesChain.Remove(namedType);
if (props != null)
if (CanLogProperties(property, property.Type, symbols, silent: logPropertiesAttribute == null))
{
lp.Properties.AddRange(props);
_ = typesChain.Add(namedType);
var props = GetTypePropertiesToLog(extractedType, typesChain, symbols, transitive, ref foundDataClassificationAttributes);
_ = typesChain.Remove(namedType);
if (props != null)
{
lp.Properties.AddRange(props);
}
}
}
@ -284,7 +297,7 @@ internal partial class Parser
}
}
if (tagProviderAttribute == null && logPropertiesAttribute == null)
if (tagProviderAttribute == null && logPropertiesAttribute == null && !transitive)
{
if ((property.DeclaredAccessibility != Accessibility.Public || property.IsStatic)
|| (property.GetMethod == null || property.GetMethod.DeclaredAccessibility != Accessibility.Public)
@ -301,6 +314,15 @@ internal partial class Parser
lp.ClassificationAttributeTypes.Clear();
}
if ((logPropertiesAttribute is null)
&& (tagProviderAttribute is null)
&& !lp.IsStringifiable
&& property.Type.Kind != SymbolKind.TypeParameter
&& !transitive)
{
Diag(DiagDescriptors.DefaultToString, property.GetLocation(), property.Type, property.Name);
}
result.Add(lp);
}
@ -311,7 +333,7 @@ internal partial class Parser
return result;
}
bool CanLogProperties(ISymbol sym, ITypeSymbol symType, SymbolHolder symbols)
bool CanLogProperties(ISymbol sym, ITypeSymbol symType, SymbolHolder symbols, bool silent = false)
{
var isRegularType =
symType.Kind == SymbolKind.NamedType &&
@ -326,7 +348,11 @@ internal partial class Parser
if (!isRegularType || symType.IsSpecialType(symbols))
{
Diag(DiagDescriptors.InvalidTypeToLogProperties, sym.GetLocation(), symType.ToDisplayString());
if (!silent)
{
Diag(DiagDescriptors.InvalidTypeToLogProperties, sym.GetLocation(), symType.ToDisplayString());
}
return false;
}

Просмотреть файл

@ -84,7 +84,7 @@ internal sealed partial class Parser
foreach (var paramSymbol in methodSymbol.Parameters)
{
var lp = ProcessParameter(paramSymbol, symbols, ref parsingState);
var lp = ProcessParameter(lm, paramSymbol, symbols, ref parsingState);
if (lp == null)
{
keepMethod = false;
@ -94,6 +94,7 @@ internal sealed partial class Parser
parameterSymbols[lp] = paramSymbol;
var foundDataClassificationAttributesInProps = false;
var logPropertiesAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.LogPropertiesAttribute, paramSymbol);
if (logPropertiesAttribute is not null)
{
@ -135,22 +136,43 @@ internal sealed partial class Parser
lp.TagProvider = null;
}
#pragma warning disable S1067 // Expressions should not be too complex
if (lp.IsNormalParameter
&& (logPropertiesAttribute is null)
&& (tagProviderAttribute is null)
&& !lp.IsStringifiable
&& paramSymbol.Type.Kind != SymbolKind.TypeParameter)
{
Diag(DiagDescriptors.DefaultToString, paramSymbol.GetLocation(), paramSymbol.Type, paramSymbol.Name);
}
#pragma warning restore S1067 // Expressions should not be too complex
bool forceAsTemplateParam = false;
bool parameterInTemplate = lm.TemplateToParameterName.ContainsKey(lp.Name);
bool parameterInTemplate = false;
foreach (var t in lm.Templates)
{
if (lp.ParameterName.Equals(t, StringComparison.OrdinalIgnoreCase))
{
parameterInTemplate = true;
break;
}
}
var loggingProperties = logPropertiesAttribute != null || tagProviderAttribute != null;
if (lp.IsLogger && parameterInTemplate)
{
Diag(DiagDescriptors.ShouldntMentionLoggerInMessage, attrLoc, lp.Name);
Diag(DiagDescriptors.ShouldntMentionLoggerInMessage, attrLoc, lp.ParameterName);
forceAsTemplateParam = true;
}
else if (lp.IsException && parameterInTemplate)
{
Diag(DiagDescriptors.ShouldntMentionExceptionInMessage, attrLoc, lp.Name);
Diag(DiagDescriptors.ShouldntMentionExceptionInMessage, attrLoc, lp.ParameterName);
forceAsTemplateParam = true;
}
else if (lp.IsLogLevel && parameterInTemplate)
{
Diag(DiagDescriptors.ShouldntMentionLogLevelInMessage, attrLoc, lp.Name);
Diag(DiagDescriptors.ShouldntMentionLogLevelInMessage, attrLoc, lp.ParameterName);
forceAsTemplateParam = true;
}
else if (lp.IsNormalParameter
@ -158,7 +180,7 @@ internal sealed partial class Parser
&& !loggingProperties
&& !string.IsNullOrEmpty(lm.Message))
{
Diag(DiagDescriptors.ParameterHasNoCorrespondingTemplate, paramSymbol.GetLocation(), lp.Name);
Diag(DiagDescriptors.ParameterHasNoCorrespondingTemplate, paramSymbol.GetLocation(), lp.ParameterName);
}
var purelyStructuredLoggingParameter = loggingProperties && !parameterInTemplate;
@ -170,7 +192,7 @@ internal sealed partial class Parser
if (foundDataClassificationAttributesInProps ||
RecordHasSensitivePublicMembers(paramSymbol.Type, symbols))
{
Diag(DiagDescriptors.RecordTypeSensitiveArgumentIsInTemplate, paramSymbol.GetLocation(), lp.Name, lm.Name);
Diag(DiagDescriptors.RecordTypeSensitiveArgumentIsInTemplate, paramSymbol.GetLocation(), lp.ParameterName, lm.Name);
keepMethod = false;
}
}
@ -246,12 +268,12 @@ internal sealed partial class Parser
}
}
foreach (var t in lm.TemplateToParameterName)
foreach (var t in lm.Templates)
{
bool found = false;
foreach (var p in lm.Parameters)
{
if (t.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase))
if (t.Equals(p.ParameterName, StringComparison.OrdinalIgnoreCase))
{
found = true;
break;
@ -260,11 +282,11 @@ internal sealed partial class Parser
if (!found)
{
Diag(DiagDescriptors.TemplateHasNoCorrespondingParameter, attrLoc, t.Key);
Diag(DiagDescriptors.TemplateHasNoCorrespondingParameter, attrLoc, t);
}
}
CheckMethodParametersAreUnique(lm, parameterSymbols);
CheckTagNamesAreUnique(lm, parameterSymbols);
}
if (lt == null)
@ -367,8 +389,6 @@ internal sealed partial class Parser
HasXmlDocumentation = HasXmlDocumentation(method),
};
TemplateExtractor.ExtractTemplates(message, lm.TemplateToParameterName, out var templatesWithAtSymbol);
var keepMethod = true;
if (!methodSymbol.ReturnsVoid)
@ -378,12 +398,26 @@ internal sealed partial class Parser
keepMethod = false;
}
if (templatesWithAtSymbol.Count > 0)
TemplateExtractor.ExtractTemplates(message, lm.Templates);
#pragma warning disable EA0003 // Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith'
var templatesWithAtSymbol = lm.Templates.Where(x => x.StartsWith("@", StringComparison.Ordinal)).ToArray();
if (templatesWithAtSymbol.Length > 0)
{
// there is/are template(s) that start with @, which is not allowed
Diag(DiagDescriptors.TemplateStartsWithAtSymbol, attrLoc, method.Identifier.Text, string.Join("; ", templatesWithAtSymbol));
keepMethod = false;
for (int i = 0; i < lm.Templates.Count; i++)
{
if (lm.Templates[i].StartsWith("@", StringComparison.Ordinal))
{
lm.Templates[i] = lm.Templates[i].Substring(1);
}
}
}
#pragma warning restore EA0003 // Use the character-based overloads of 'String.StartsWith' or 'String.EndsWith'
if (method.Arity > 0)
{
@ -462,15 +496,14 @@ internal sealed partial class Parser
.Select(static x => x!)
.ToList();
private void CheckMethodParametersAreUnique(LoggingMethod lm, Dictionary<LoggingMethodParameter, IParameterSymbol> parameterSymbols)
private void CheckTagNamesAreUnique(LoggingMethod lm, Dictionary<LoggingMethodParameter, IParameterSymbol> parameterSymbols)
{
var names = new HashSet<string>(StringComparer.Ordinal);
foreach (var parameter in lm.Parameters)
{
var parameterName = lm.GetParameterNameInTemplate(parameter);
if (!names.Add(parameterName))
if (!parameter.IsNormalParameter)
{
Diag(DiagDescriptors.LogPropertiesNameCollision, parameterSymbols[parameter].GetLocation(), parameter.Name, parameterName, lm.Name);
continue;
}
if (parameter.HasProperties)
@ -482,17 +515,25 @@ internal sealed partial class Parser
chain = chain.Skip(1);
}
var fullName = string.Join("_", chain.Concat(new[] { leaf }).Select(static x => x.Name));
var fullName = string.Join("_", chain.Concat(new[] { leaf }).Select(static x => x.TagName));
if (!names.Add(fullName))
{
Diag(DiagDescriptors.LogPropertiesNameCollision, parameterSymbols[parameter].GetLocation(), parameter.Name, fullName, lm.Name);
Diag(DiagDescriptors.TagNameCollision, parameterSymbols[parameter].GetLocation(), parameter.ParameterName, fullName, lm.Name);
}
});
}
else
{
if (!names.Add(parameter.TagName))
{
Diag(DiagDescriptors.TagNameCollision, parameterSymbols[parameter].GetLocation(), parameter.ParameterName, parameter.TagName, lm.Name);
}
}
}
}
private LoggingMethodParameter? ProcessParameter(
LoggingMethod lm,
IParameterSymbol paramSymbol,
SymbolHolder symbols,
ref MethodParsingState parsingState)
@ -550,9 +591,15 @@ internal sealed partial class Parser
extractedType = ((INamedTypeSymbol)paramTypeSymbol).TypeArguments[0];
}
var tagNameAttribute = ParserUtilities.GetSymbolAttributeAnnotationOrDefault(symbols.TagNameAttribute, paramSymbol);
var tagName = tagNameAttribute != null
? AttributeProcessors.ExtractTagNameAttributeValues(tagNameAttribute)
: lm.GetTemplatesForParameter(paramName).FirstOrDefault() ?? paramName;
var lp = new LoggingMethodParameter
{
Name = paramName,
ParameterName = paramName,
TagName = tagName,
Type = typeName,
Qualifier = qualifier,
NeedsAtSign = needsAtSign,
@ -566,6 +613,7 @@ internal sealed partial class Parser
ImplementsIConvertible = paramTypeSymbol.ImplementsIConvertible(symbols),
ImplementsIFormattable = paramTypeSymbol.ImplementsIFormattable(symbols),
ImplementsISpanFormattable = paramTypeSymbol.ImplementsISpanFormattable(symbols),
HasCustomToString = paramTypeSymbol.HasCustomToString(),
};
parsingState.FoundLogger |= lp.IsLogger;

Просмотреть файл

@ -96,6 +96,24 @@ namespace Microsoft.Gen.Logging.Parsing {
}
}
/// <summary>
/// Looks up a localized string similar to The type &quot;{0}&quot; doesn&apos;t implement ToString(), IConvertible, or IFormattable (did you forget to apply [LogProperties] or [TagProvider] to &quot;{1}&quot;?).
/// </summary>
internal static string DefaultToStringMessage {
get {
return ResourceManager.GetString("DefaultToStringMessage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A value being logged doesn&apos;t have an effective way to be converted into a string.
/// </summary>
internal static string DefaultToStringTitle {
get {
return ResourceManager.GetString("DefaultToStringTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Logging method &quot;{0}&quot; doesn&apos;t have anything to be logged.
/// </summary>
@ -312,24 +330,6 @@ namespace Microsoft.Gen.Logging.Parsing {
}
}
/// <summary>
/// Looks up a localized string similar to Parameter &quot;{0}&quot; causes name conflict with name &quot;{1}&quot; within logging method &quot;{2}&quot;.
/// </summary>
internal static string LogPropertiesNameCollisionMessage {
get {
return ResourceManager.GetString("LogPropertiesNameCollisionMessage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A logging method parameter causes name conflicts.
/// </summary>
internal static string LogPropertiesNameCollisionTitle {
get {
return ResourceManager.GetString("LogPropertiesNameCollisionTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type &quot;{0}&quot; used with parameter &quot;{1}&quot; doesn&apos;t have any public properties to log.
/// </summary>
@ -582,6 +582,24 @@ namespace Microsoft.Gen.Logging.Parsing {
}
}
/// <summary>
/// Looks up a localized string similar to Parameter &quot;{0}&quot; causes a tag name conflict with name &quot;{1}&quot; within logging method &quot;{2}&quot;.
/// </summary>
internal static string TagNameCollisionMessage {
get {
return ResourceManager.GetString("TagNameCollisionMessage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A logging method parameter causes a tag name conflicts.
/// </summary>
internal static string TagNameCollisionTitle {
get {
return ResourceManager.GetString("TagNameCollisionTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Parameter &quot;{0}&quot; is annotated to use a tag provider but it has special semantics (ILogger, LogLevel, Exception, etc.).
/// </summary>

Просмотреть файл

@ -282,11 +282,11 @@
<data name="LogPropertiesHiddenPropertyDetectedTitle" xml:space="preserve">
<value>Logging method parameter's type has a hidden property</value>
</data>
<data name="LogPropertiesNameCollisionMessage" xml:space="preserve">
<value>Parameter "{0}" causes name conflict with name "{1}" within logging method "{2}"</value>
<data name="TagNameCollisionMessage" xml:space="preserve">
<value>Parameter "{0}" causes a tag name conflict with name "{1}" within logging method "{2}"</value>
</data>
<data name="LogPropertiesNameCollisionTitle" xml:space="preserve">
<value>A logging method parameter causes name conflicts</value>
<data name="TagNameCollisionTitle" xml:space="preserve">
<value>A logging method parameter causes a tag name conflicts</value>
</data>
<data name="EmptyLoggingMethodTitle" xml:space="preserve">
<value>Logging method doesn't log anything</value>
@ -333,4 +333,10 @@
<data name="RecordTypeSensitiveArgumentIsInTemplateTitle" xml:space="preserve">
<value>The logging method parameter leaks sensitive data</value>
</data>
<data name="DefaultToStringMessage" xml:space="preserve">
<value>The type "{0}" doesn't implement ToString(), IConvertible, or IFormattable (did you forget to apply [LogProperties] or [TagProvider] to "{1}"?)</value>
</data>
<data name="DefaultToStringTitle" xml:space="preserve">
<value>A value being logged doesn't have an effective way to be converted into a string</value>
</data>
</root>

Просмотреть файл

@ -13,7 +13,8 @@ internal sealed record class SymbolHolder(
INamedTypeSymbol LoggerMessageAttribute,
INamedTypeSymbol LogPropertiesAttribute,
INamedTypeSymbol TagProviderAttribute,
INamedTypeSymbol? LogPropertyIgnoreAttribute,
INamedTypeSymbol TagNameAttribute,
INamedTypeSymbol LogPropertyIgnoreAttribute,
INamedTypeSymbol ITagCollectorSymbol,
INamedTypeSymbol ILoggerSymbol,
INamedTypeSymbol LogLevelSymbol,

Просмотреть файл

@ -12,6 +12,7 @@ internal static class SymbolLoader
internal const string LoggerMessageAttribute = "Microsoft.Extensions.Logging.LoggerMessageAttribute";
internal const string LogPropertiesAttribute = "Microsoft.Extensions.Logging.LogPropertiesAttribute";
internal const string TagProviderAttribute = "Microsoft.Extensions.Logging.TagProviderAttribute";
internal const string TagNameAttribute = "Microsoft.Extensions.Logging.TagNameAttribute";
internal const string LogPropertyIgnoreAttribute = "Microsoft.Extensions.Logging.LogPropertyIgnoreAttribute";
internal const string ITagCollectorType = "Microsoft.Extensions.Logging.ITagCollector";
internal const string ILoggerType = "Microsoft.Extensions.Logging.ILogger";
@ -55,6 +56,7 @@ internal static class SymbolLoader
var loggerMessageAttributeSymbol = compilation.GetTypeByMetadataName(LoggerMessageAttribute);
var logPropertiesAttributeSymbol = compilation.GetTypeByMetadataName(LogPropertiesAttribute);
var tagProviderAttributeSymbol = compilation.GetTypeByMetadataName(TagProviderAttribute);
var tagNameAttributeSymbol = compilation.GetTypeByMetadataName(TagNameAttribute);
var tagCollectorSymbol = compilation.GetTypeByMetadataName(ITagCollectorType);
var logPropertyIgnoreAttributeSymbol = compilation.GetTypeByMetadataName(LogPropertyIgnoreAttribute);
var dataClassificationAttribute = compilation.GetTypeByMetadataName(DataClassificationAttribute);
@ -65,6 +67,7 @@ internal static class SymbolLoader
|| loggerMessageAttributeSymbol == null
|| logPropertiesAttributeSymbol == null
|| tagProviderAttributeSymbol == null
|| tagNameAttributeSymbol == null
|| tagCollectorSymbol == null
|| logPropertyIgnoreAttributeSymbol == null)
{
@ -100,6 +103,7 @@ internal static class SymbolLoader
loggerMessageAttributeSymbol,
logPropertiesAttributeSymbol,
tagProviderAttributeSymbol,
tagNameAttributeSymbol,
logPropertyIgnoreAttributeSymbol,
tagCollectorSymbol,
loggerSymbol,

Просмотреть файл

@ -13,19 +13,15 @@ internal static class TemplateExtractor
/// <summary>
/// Finds the template arguments contained in the message string.
/// </summary>
internal static void ExtractTemplates(string? message, IDictionary<string, string> templateToParameterName, out ICollection<string> templatesWithAtSymbol)
internal static void ExtractTemplates(string? message, List<string> templates)
{
if (string.IsNullOrEmpty(message))
{
templatesWithAtSymbol = Array.Empty<string>();
return;
}
var scanIndex = 0;
var endIndex = message!.Length;
#pragma warning disable CA1859 // Use concrete types when possible for improved performance
ICollection<string>? foundAtTemplates = null;
#pragma warning restore CA1859 // Use concrete types when possible for improved performance
while (scanIndex < endIndex)
{
var openBraceIndex = FindBraceIndex(message, '{', scanIndex, endIndex);
@ -41,19 +37,10 @@ internal static class TemplateExtractor
var formatDelimiterIndex = FindIndexOfAny(message, _formatDelimiters, openBraceIndex, closeBraceIndex);
var templateName = message.Substring(openBraceIndex + 1, formatDelimiterIndex - openBraceIndex - 1).Trim();
if (templateName[0] == '@')
{
foundAtTemplates ??= new List<string>();
foundAtTemplates.Add(templateName);
templateName = templateName.Substring(1);
}
templateToParameterName[templateName] = templateName;
templates.Add(templateName);
scanIndex = closeBraceIndex + 1;
}
}
templatesWithAtSymbol = foundAtTemplates ?? Array.Empty<string>();
}
internal static int FindIndexOfAny(string message, char[] chars, int startIndex, int endIndex)

Просмотреть файл

@ -10,7 +10,8 @@ namespace Microsoft.Gen.Logging.Parsing;
internal static class TypeSymbolExtensions
{
internal static bool IsEnumerable(this ITypeSymbol sym, SymbolHolder symbols)
=> sym.ImplementsInterface(symbols.EnumerableSymbol) && sym.SpecialType != SpecialType.System_String;
=> (sym.ImplementsInterface(symbols.EnumerableSymbol) || SymbolEqualityComparer.Default.Equals(sym, symbols.EnumerableSymbol))
&& sym.SpecialType != SpecialType.System_String;
internal static bool ImplementsIConvertible(this ITypeSymbol sym, SymbolHolder symbols)
{
@ -56,7 +57,7 @@ internal static class TypeSymbolExtensions
}
internal static bool ImplementsISpanFormattable(this ITypeSymbol sym, SymbolHolder symbols)
=> symbols.SpanFormattableSymbol != null && sym.ImplementsInterface(symbols.SpanFormattableSymbol);
=> symbols.SpanFormattableSymbol != null && (sym.ImplementsInterface(symbols.SpanFormattableSymbol) || SymbolEqualityComparer.Default.Equals(sym, symbols.SpanFormattableSymbol));
internal static bool IsSpecialType(this ITypeSymbol typeSymbol, SymbolHolder symbols)
=> typeSymbol.SpecialType != SpecialType.None ||
@ -64,4 +65,21 @@ internal static class TypeSymbolExtensions
#pragma warning disable RS1024
symbols.IgnorePropertiesSymbols.Contains(typeSymbol);
#pragma warning restore RS1024
internal static bool HasCustomToString(this ITypeSymbol type)
{
ITypeSymbol? current = type;
while (current != null && current.SpecialType != SpecialType.System_Object)
{
if (current.GetMembers("ToString").Where(m => m.Kind == SymbolKind.Method && m.DeclaredAccessibility == Accessibility.Public).Cast<IMethodSymbol>().Any(m => m.Parameters.Length == 0))
{
return true;
}
current = current.BaseType;
}
return false;
}
}

Просмотреть файл

@ -3,12 +3,14 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.DiagnosticIds;
namespace Microsoft.Extensions.Logging;
/// <summary>
/// Marks a logging method parameter whose public tags need to be logged.
/// Marks a logging method parameter whose public properties need to be logged as log tags.
/// </summary>
/// <seealso cref="LoggerMessageAttribute"/>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
@ -16,7 +18,7 @@ namespace Microsoft.Extensions.Logging;
public sealed class LogPropertiesAttribute : Attribute
{
/// <summary>
/// Gets or sets a value indicating whether <see langword="null"/> tags are logged.
/// Gets or sets a value indicating whether <see langword="null"/> properties are logged.
/// </summary>
/// <value>
/// Defaults to <see langword="false"/>.
@ -30,4 +32,19 @@ public sealed class LogPropertiesAttribute : Attribute
/// Defaults to <see langword="false"/>.
/// </value>
public bool OmitReferenceName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to transitively visit properties which are complex objects.
/// </summary>
/// <remarks>
/// When logging the properties of an object, this property controls the behavior for each encountered property.
/// When this property is <see langword="false"/>, then each property is serialized by calling <see cref="object.ToString" /> to
/// generate a string for the property. When this property is <see langword="true"/>, then each property of any complex objects are
/// expanded individually.
/// </remarks>
/// <value>
/// Defaults to <see langword="false"/>.
/// </value>
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public bool Transitive { get; set; }
}

Просмотреть файл

@ -0,0 +1,34 @@
// 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 System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Shared.DiagnosticIds;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.Logging;
/// <summary>
/// Defines the tag name to use for a logged parameter or property.
/// </summary>
/// <seealso cref="LoggerMessageAttribute"/>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]
[Conditional("CODE_GENERATION_ATTRIBUTES")]
[Experimental(diagnosticId: DiagnosticIds.Experiments.Telemetry, UrlFormat = DiagnosticIds.UrlFormat)]
public sealed class TagNameAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="TagNameAttribute"/> class.
/// </summary>
/// <param name="name">The tag name to use when logging the annotated parameter or property.</param>
public TagNameAttribute(string name)
{
Name = Throw.IfNull(name);
}
/// <summary>
/// Gets the name of the tag to be used when logging the parameter or property.
/// </summary>
public string Name { get; }
}

Просмотреть файл

@ -28,32 +28,6 @@ internal static class DiagnosticIds
internal const string CTXOPTGEN003 = nameof(CTXOPTGEN003);
}
internal static class Design
{
internal const string AUTOCLIENTGEN001 = nameof(AUTOCLIENTGEN001);
internal const string AUTOCLIENTGEN002 = nameof(AUTOCLIENTGEN002);
internal const string AUTOCLIENTGEN003 = nameof(AUTOCLIENTGEN003);
internal const string AUTOCLIENTGEN004 = nameof(AUTOCLIENTGEN004);
internal const string AUTOCLIENTGEN005 = nameof(AUTOCLIENTGEN005);
internal const string AUTOCLIENTGEN006 = nameof(AUTOCLIENTGEN006);
internal const string AUTOCLIENTGEN007 = nameof(AUTOCLIENTGEN007);
internal const string AUTOCLIENTGEN008 = nameof(AUTOCLIENTGEN008);
internal const string AUTOCLIENTGEN009 = nameof(AUTOCLIENTGEN009);
internal const string AUTOCLIENTGEN010 = nameof(AUTOCLIENTGEN010);
internal const string AUTOCLIENTGEN011 = nameof(AUTOCLIENTGEN011);
internal const string AUTOCLIENTGEN012 = nameof(AUTOCLIENTGEN012);
internal const string AUTOCLIENTGEN013 = nameof(AUTOCLIENTGEN013);
internal const string AUTOCLIENTGEN014 = nameof(AUTOCLIENTGEN014);
internal const string AUTOCLIENTGEN015 = nameof(AUTOCLIENTGEN015);
internal const string AUTOCLIENTGEN016 = nameof(AUTOCLIENTGEN016);
internal const string AUTOCLIENTGEN017 = nameof(AUTOCLIENTGEN017);
internal const string AUTOCLIENTGEN018 = nameof(AUTOCLIENTGEN018);
internal const string AUTOCLIENTGEN019 = nameof(AUTOCLIENTGEN019);
internal const string AUTOCLIENTGEN020 = nameof(AUTOCLIENTGEN020);
internal const string AUTOCLIENTGEN021 = nameof(AUTOCLIENTGEN021);
internal const string AUTOCLIENTGEN022 = nameof(AUTOCLIENTGEN022);
}
/// <summary>
/// Experiments supported by this repo.
/// </summary>
@ -63,7 +37,6 @@ internal static class DiagnosticIds
internal const string Compliance = "EXTEXP0002";
internal const string Telemetry = "EXTEXP0003";
internal const string TimeProvider = "EXTEXP0004";
internal const string AutoClient = "EXTEXP0005";
internal const string AsyncState = "EXTEXP0006";
internal const string HealthChecks = "EXTEXP0007";
internal const string ResourceMonitoring = "EXTEXP0008";
@ -112,6 +85,7 @@ internal static class DiagnosticIds
internal const string LOGGEN033 = nameof(LOGGEN033);
internal const string LOGGEN034 = nameof(LOGGEN034);
internal const string LOGGEN035 = nameof(LOGGEN035);
internal const string LOGGEN036 = nameof(LOGGEN036);
}
internal static class Metrics

Просмотреть файл

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Logging.Testing;
using TestClasses;
using Xunit;
namespace Microsoft.Gen.Logging.Test;
public class TagNameTests
{
[Fact]
public void Basic()
{
var logger = new FakeLogger();
TagNameExtensions.M0(logger, 0);
var expectedState = new Dictionary<string, string?>
{
["TN1"] = "0",
};
logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
}
}

Просмотреть файл

@ -0,0 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Extensions.Logging.Testing;
using TestClasses;
using Xunit;
namespace Microsoft.Gen.Logging.Test;
public class TransitiveTests
{
[Fact]
public void Basic()
{
var logger = new FakeLogger();
var c = new TransitiveTestExtensions.C0();
TransitiveTestExtensions.M0(logger, c);
var expectedState = new Dictionary<string, string?>
{
["p0.P1"] = "V1",
["p0.P0.P2"] = "V2",
};
logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
TransitiveTestExtensions.M1(logger, c);
expectedState = new Dictionary<string, string?>
{
["p0.P1"] = "V1",
["p0.P0"] = "TS1",
};
logger.Collector.LatestRecord.StructuredState.Should().NotBeNull().And.Equal(expectedState);
}
}

Просмотреть файл

@ -19,7 +19,9 @@ namespace TestClasses
public Version? P4 { get; set; }
public Uri? P5 { get; set; }
public IPAddress? P6 { get; set; }
#pragma warning disable LOGGEN036
public EndPoint? P7 { get; set; }
#pragma warning restore LOGGEN036
public IPEndPoint? P8 { get; set; }
public DnsEndPoint? P9 { get; set; }
public BigInteger P10 { get; set; }

Просмотреть файл

@ -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.
using Microsoft.Extensions.Logging;
namespace TestClasses
{
internal static partial class TagNameExtensions
{
[LoggerMessage(LogLevel.Warning)]
internal static partial void M0(ILogger logger, [TagName("TN1")] int p0);
}
}

Просмотреть файл

@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Logging;
namespace TestClasses
{
internal static partial class TransitiveTestExtensions
{
public class C0
{
public C1 P0 { get; set; } = new C1();
public string P1 { get; set; } = "V1";
}
public class C1
{
public string P2 { get; set; } = "V2";
public override string ToString() => "TS1";
}
[LoggerMessage(LogLevel.Debug)]
public static partial void M0(ILogger logger, [LogProperties(Transitive = true)] C0 p0);
[LoggerMessage(LogLevel.Debug)]
public static partial void M1(ILogger logger, [LogProperties(Transitive = false)] C0 p0);
}
}

Просмотреть файл

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.Compliance.Classification;
@ -44,6 +45,7 @@ public class EmitterTests
Assembly.GetAssembly(typeof(DataClassification))!,
Assembly.GetAssembly(typeof(IRedactorProvider))!,
Assembly.GetAssembly(typeof(PrivateDataAttribute))!,
Assembly.GetAssembly(typeof(BigInteger))!,
},
sources,
symbols)

Просмотреть файл

@ -58,7 +58,7 @@ public class EmitterUtilsTests
lm.Parameters.Add(new LoggingMethodParameter
{
IsLogLevel = true,
Name = ParamName
ParameterName = ParamName
});
Assert.Equal(ParamName, Emitter.GetLoggerMethodLogLevel(lm));

Просмотреть файл

@ -64,6 +64,7 @@ public class LogParserUtilitiesTests
null!,
null!,
null!,
null!,
null!);
var diagMock = new Mock<Action<Diagnostic>>();
@ -110,6 +111,7 @@ public class LogParserUtilitiesTests
null!,
null!,
null!,
null!,
Mock.Of<INamedTypeSymbol>());
var diagMock = new Mock<Action<Diagnostic>>();
@ -134,6 +136,7 @@ public class LogParserUtilitiesTests
null!,
null!,
null!,
null!,
new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default),
null!,
null!,

Просмотреть файл

@ -14,7 +14,7 @@ public class LoggingMethodParameterTests
public void Fields_Should_BeInitialized()
{
var instance = new LoggingMethodParameter();
Assert.Empty(instance.Name);
Assert.Empty(instance.ParameterName);
Assert.Empty(instance.Type);
}
@ -57,13 +57,13 @@ public class LoggingMethodParameterTests
{
var lp = new LoggingMethodParameter
{
Name = "Foo",
ParameterName = "Foo",
NeedsAtSign = false,
};
Assert.Equal(lp.Name, lp.NameWithAt);
Assert.Equal(lp.ParameterName, lp.ParameterNameWithAt);
lp.NeedsAtSign = true;
Assert.Equal("@" + lp.Name, lp.NameWithAt);
Assert.Equal("@" + lp.ParameterName, lp.ParameterNameWithAt);
lp.Type = "Foo";
lp.IsReference = false;

Просмотреть файл

@ -17,22 +17,4 @@ public class LoggingMethodTests
Assert.Empty(instance.Modifiers);
Assert.Equal("_logger", instance.LoggerMember);
}
[Fact]
public void ShouldReturnParameterNameIfNotFoundInMap()
{
var p = new LoggingMethodParameter { Name = "paramName" };
var method = new LoggingMethod();
Assert.Equal(p.Name, method.GetParameterNameInTemplate(p));
}
[Fact]
public void ShouldReturnNameForParameterFromMap()
{
var p = new LoggingMethodParameter { Name = "paramName" };
var method = new LoggingMethod();
method.TemplateToParameterName[p.Name] = "Name from the map";
Assert.Equal(method.TemplateToParameterName[p.Name], method.GetParameterNameInTemplate(p));
}
}

Просмотреть файл

@ -53,11 +53,11 @@ public partial class ParserTests
partial class C
{
[LoggerMessage(0, LogLevel.Debug, ""Parameterless..."")]
static partial void M0(ILogger logger, [LogProperties(OmitReferenceName = true)] MyType /*0+*/p0/*-0*/);
[LoggerMessage(LogLevel.Debug)]
static partial void M0(ILogger logger, int p0, [LogProperties(OmitReferenceName = true)] MyType /*0+*/p1/*-0*/);
}";
await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision);
await RunGenerator(Source, DiagDescriptors.TagNameCollision);
}
[Fact]
@ -207,7 +207,7 @@ public partial class ParserTests
static partial void M(ILogger logger, string param, string /*0+*/Param/*-0*/);
}";
await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision);
await RunGenerator(Source, DiagDescriptors.TagNameCollision);
}
[Fact]
@ -217,15 +217,21 @@ public partial class ParserTests
class MyClass
{
public int A { get; set; }
[LogPropertyIgnore]
public int B { get; set; }
}
partial class C
{
[LoggerMessage(0, LogLevel.Debug, ""{param_A}"")]
static partial void M(ILogger logger, string param_A, [LogProperties] MyClass /*0+*/param/*-0*/);
[LoggerMessage(LogLevel.Debug)]
static partial void M0(ILogger logger, string param_A, [LogProperties] MyClass /*0+*/param/*-0*/);
[LoggerMessage(LogLevel.Debug)]
static partial void M1(ILogger logger, string param_B, [LogProperties] MyClass param);
}";
await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision);
await RunGenerator(Source, DiagDescriptors.TagNameCollision);
}
[Fact]
@ -251,7 +257,7 @@ public partial class ParserTests
static partial void M(ILogger logger, [LogProperties] MyClass /*0+*/param/*-0*/);
}";
await RunGenerator(Source, DiagDescriptors.LogPropertiesNameCollision);
await RunGenerator(Source, DiagDescriptors.TagNameCollision);
}
[Theory]
@ -387,4 +393,44 @@ public partial class ParserTests
await RunGenerator(Source, DiagDescriptors.LogPropertiesHiddenPropertyDetected);
}
[Fact]
public async Task DefaultToString()
{
await RunGenerator(@"
record class MyRecordClass(int x);
record struct MyRecordStruct(int x);
class MyClass2
{
}
class MyClass3
{
public override string ToString() => ""FIND ME!"";
}
class MyClass<T>
{
public object /*0+*/P0/*-0*/ { get; set; }
public MyClass2 /*1+*/P1/*-1*/ { get; set; }
public MyClass3 P2 { get; set; }
public int P3 { get; set; }
public System.Numerics.BigInteger P4 { get; set; }
public T P5 { get; set; }
}
partial class C<T>
{
[LoggerMessage(LogLevel.Debug)]
static partial void M0(this ILogger logger,
object /*2+*/p0/*-2*/,
MyClass2 /*3+*/p1/*-3*/,
MyClass3 p2,
[LogProperties] MyClass<int> p3,
T p4,
MyRecordClass p5,
MyRecordStruct p6);
}", DiagDescriptors.DefaultToString);
}
}

Просмотреть файл

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Numerics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@ -1005,6 +1006,7 @@ public partial class ParserTests
Assembly.GetAssembly(typeof(IEnrichmentTagCollector))!,
Assembly.GetAssembly(typeof(DataClassification))!,
Assembly.GetAssembly(typeof(PrivateDataAttribute))!,
Assembly.GetAssembly(typeof(BigInteger))!,
};
}

Просмотреть файл

@ -26,4 +26,14 @@ public class LogPropertiesAttributeTests
lpa.OmitReferenceName = true;
Assert.True(lpa.OmitReferenceName);
}
[Fact]
public void Transitive()
{
var lpa = new LogPropertiesAttribute();
Assert.False(lpa.Transitive);
lpa.Transitive = true;
Assert.True(lpa.Transitive);
}
}

Просмотреть файл

@ -0,0 +1,19 @@
// 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 Xunit;
namespace Microsoft.Extensions.Logging.Test;
public class TagNameAttributeTests
{
[Fact]
public void Basic()
{
var a = new TagNameAttribute("a");
Assert.Equal("a", a.Name);
Assert.Throws<ArgumentNullException>(() => new TagNameAttribute(null!));
}
}