зеркало из https://github.com/dotnet/extensions.git
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:
Родитель
40fdaf9b31
Коммит
f4315cd121
|
@ -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 "{0}" doesn't implement ToString(), IConvertible, or IFormattable (did you forget to apply [LogProperties] or [TagProvider] to "{1}"?).
|
||||
/// </summary>
|
||||
internal static string DefaultToStringMessage {
|
||||
get {
|
||||
return ResourceManager.GetString("DefaultToStringMessage", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to A value being logged doesn'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 "{0}" doesn'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 "{0}" causes name conflict with name "{1}" within logging method "{2}".
|
||||
/// </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 "{0}" used with parameter "{1}" doesn'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 "{0}" causes a tag name conflict with name "{1}" within logging method "{2}".
|
||||
/// </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 "{0}" 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!));
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче