diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/ComplianceReportsGenerator.cs similarity index 74% rename from src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs rename to src/Generators/Microsoft.Gen.ComplianceReports/Common/ComplianceReportsGenerator.cs index 19bc8b1a3cc..f2823832f6a 100644 --- a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Generator.cs +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/ComplianceReportsGenerator.cs @@ -14,21 +14,32 @@ namespace Microsoft.Gen.ComplianceReports; /// [Generator] [ExcludeFromCodeCoverage] -public sealed class Generator : ISourceGenerator +public sealed class ComplianceReportsGenerator : ISourceGenerator { private const string GenerateComplianceReportsMSBuildProperty = "build_property.GenerateComplianceReport"; private const string ComplianceReportOutputPathMSBuildProperty = "build_property.ComplianceReportOutputPath"; - private string? _reportOutputPath; + private const string FallbackFileName = "ComplianceReport.json"; - public Generator() + private string? _directory; + private string _fileName; + + public ComplianceReportsGenerator() : this(null) { } - public Generator(string? reportOutputPath) + public ComplianceReportsGenerator(string? filePath) { - _reportOutputPath = reportOutputPath; + if (filePath is not null) + { + _directory = Path.GetDirectoryName(filePath); + _fileName = Path.GetFileName(filePath); + } + else + { + _fileName = FallbackFileName; + } } public void Initialize(GeneratorInitializationContext context) @@ -70,10 +81,10 @@ public void Execute(GeneratorExecutionContext context) context.CancellationToken.ThrowIfCancellationRequested(); - if (_reportOutputPath == null) + if (_directory == null) { - _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(ComplianceReportOutputPathMSBuildProperty, out _reportOutputPath); - if (string.IsNullOrWhiteSpace(_reportOutputPath)) + _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(ComplianceReportOutputPathMSBuildProperty, out _directory); + if (string.IsNullOrWhiteSpace(_directory)) { // no valid output path return; @@ -81,10 +92,10 @@ public void Execute(GeneratorExecutionContext context) } #pragma warning disable R9A017 // Switch to an asynchronous method for increased performance. - _ = Directory.CreateDirectory(Path.GetDirectoryName(_reportOutputPath)); + _ = Directory.CreateDirectory(_directory); - // Write properties to CSV file. - File.WriteAllText(_reportOutputPath, report); + // Write report as JSON file. + File.WriteAllText(Path.Combine(_directory, _fileName), report); #pragma warning restore R9A017 // Switch to an asynchronous method for increased performance. } } diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs index 0e40c72664c..6d2d6b63f5d 100644 --- a/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/Parser.cs @@ -47,6 +47,12 @@ public IReadOnlyList GetClassifiedTypes(IEnumerable? classifiedMembers = null; + // include the annotations provided in record constructor parameters + if (typeSyntax is RecordDeclarationSyntax recordSyntax) + { + classifiedMembers = GetClassifiedMembers(recordSyntax, classifiedMembers, sm); + } + // grab the annotated members classifiedMembers = GetClassifiedMembers(typeSymbol, classifiedMembers); @@ -94,6 +100,43 @@ private static string FormatType(ITypeSymbol typeSymbol) return result; } + private Dictionary? GetClassifiedMembers(RecordDeclarationSyntax recordSyntax, + Dictionary? classifiedMembers, + SemanticModel sm) + { + var parameters = recordSyntax.ParameterList?.Parameters; + if (parameters != null) + { + foreach (var parameter in parameters) + { + var ps = sm.GetDeclaredSymbol(parameter); + if (ps == null) + { + continue; + } + + foreach (var attribute in ps!.GetAttributes()) + { + ClassifiedItem? ci = null; + ci = AppendAttributeClassifications(ci, attribute); + if (ci != null) + { + FileLinePositionSpan fileLine = ps.Locations[0].GetLineSpan(); + ci.SourceFilePath = fileLine.Path; + ci.SourceLine = fileLine.StartLinePosition.Line + 1; + ci.Name = ps.Name; + ci.TypeName = FormatType(ps.Type); + + classifiedMembers ??= new(); + classifiedMembers[ci.Name] = ci; + } + } + } + } + + return classifiedMembers; + } + private Dictionary? GetClassifiedMembers(ITypeSymbol typeSymbol, Dictionary? classifiedMembers) { foreach (var property in typeSymbol.GetMembers().OfType()) @@ -131,7 +174,7 @@ private static string FormatType(ITypeSymbol typeSymbol) ci = AppendAttributeClassifications(ci, attribute); } - // classificaiton coming from the member's attributes + // classification coming from the member's attributes foreach (AttributeData attribute in member.GetAttributes()) { ci = AppendAttributeClassifications(ci, attribute); @@ -188,7 +231,7 @@ private static string FormatType(ITypeSymbol typeSymbol) ci = AppendAttributeClassifications(ci, attribute); } - // classificaiton coming from the parameter's attributes + // classification coming from the parameter's attributes foreach (AttributeData attribute in p.GetAttributes()) { ci = AppendAttributeClassifications(ci, attribute); diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs index dd8f5c27817..ac3cae4c536 100644 --- a/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Common/SymbolHolder.cs @@ -6,7 +6,7 @@ namespace Microsoft.Gen.ComplianceReports; /// -/// Holds required symbols for the . +/// Holds required symbols for the . /// internal sealed record class SymbolHolder( INamedTypeSymbol DataClassificationAttributeSymbol, diff --git a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs b/src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextualOptionsGenerator.cs similarity index 97% rename from src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs rename to src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextualOptionsGenerator.cs index 7d731c67f81..c05b4719443 100644 --- a/src/Generators/Microsoft.Gen.ContextualOptions/Common/Generator.cs +++ b/src/Generators/Microsoft.Gen.ContextualOptions/Common/ContextualOptionsGenerator.cs @@ -15,7 +15,7 @@ namespace Microsoft.Gen.ContextualOptions; [Generator] [ExcludeFromCodeCoverage] -public class Generator : IIncrementalGenerator +public class ContextualOptionsGenerator : IIncrementalGenerator { private static readonly HashSet _attributeNames = new() { @@ -78,7 +78,7 @@ namespace Microsoft.Gen.ContextualOptions; /// [Generator] [ExcludeFromCodeCoverage] -public class Generator : ISourceGenerator +public class ContextualOptionsGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) => context.RegisterForSyntaxNotifications(() => new ContextReceiver(context.CancellationToken)); diff --git a/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs b/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs index b44b2eb3d1d..2a968844173 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs +++ b/src/Generators/Microsoft.Gen.Metering/Common/DiagDescriptors.cs @@ -113,4 +113,16 @@ internal sealed class DiagDescriptors : DiagDescriptorsBase title: Resources.ErrorInvalidMethodReturnTypeArityTitle, messageFormat: Resources.ErrorInvalidMethodReturnTypeArityMessage, category: Category); + + public static DiagnosticDescriptor ErrorGaugeNotSupported { get; } = Make( + id: "R9G069", + title: Resources.ErrorGaugeNotSupportedTitle, + messageFormat: Resources.ErrorGaugeNotSupportedMessage, + category: Category); + + public static DiagnosticDescriptor ErrorXmlNotLoadedCorrectly { get; } = Make( + id: "R9G070", + title: Resources.ErrorXmlNotLoadedCorrectlyTitle, + messageFormat: Resources.ErrorXmlNotLoadedCorrectlyMessage, + category: Category); } diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Generator.cs b/src/Generators/Microsoft.Gen.Metering/Common/MeteringGenerator.cs similarity index 93% rename from src/Generators/Microsoft.Gen.Metering/Common/Generator.cs rename to src/Generators/Microsoft.Gen.Metering/Common/MeteringGenerator.cs index e26af738b21..52e57de3ba6 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/Generator.cs +++ b/src/Generators/Microsoft.Gen.Metering/Common/MeteringGenerator.cs @@ -16,14 +16,15 @@ namespace Microsoft.Gen.Metering; [Generator] [ExcludeFromCodeCoverage] -public class Generator : IIncrementalGenerator +public class MeteringGenerator : IIncrementalGenerator { private static readonly HashSet _attributeNames = new() { SymbolLoader.CounterAttribute, SymbolLoader.CounterTAttribute.Replace("`1", ""), SymbolLoader.HistogramAttribute, - SymbolLoader.HistogramTAttribute.Replace("`1", "") + SymbolLoader.HistogramTAttribute.Replace("`1", ""), + SymbolLoader.GaugeAttribute }; public void Initialize(IncrementalGeneratorInitializationContext context) @@ -55,13 +56,14 @@ private static void HandleAnnotatedTypes(Compilation compilation, IEnumerable AllParameters = new(); public HashSet DimensionsKeys = new(); + public Dictionary DimensionDescriptionDictionary = new(); public string? Name; public string? MetricName; + public string? XmlDefinition; public bool IsExtensionMethod; public string Modifiers = string.Empty; public string MetricTypeModifiers = string.Empty; diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs b/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs index 61237c5005e..575d995ac52 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs +++ b/src/Generators/Microsoft.Gen.Metering/Common/Parser.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Xml; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -94,7 +95,7 @@ public IReadOnlyList GetMetricClasses(IEnumerable dimensions) ExtractAttributeParameters(AttributeData attribute) + private (string metricName, HashSet dimensions, Dictionary dimensionDescriptions) ExtractAttributeParameters( + AttributeData attribute, + SemanticModel semanticModel) { var dimensionHashSet = new HashSet(); + var dimensionDescriptionMap = new Dictionary(); string metricNameFromAttribute = string.Empty; if (!attribute.NamedArguments.IsDefaultOrEmpty) { @@ -323,7 +331,72 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP } } - return (metricNameFromAttribute, dimensionHashSet); + if (attribute.ApplicationSyntaxReference != null && + attribute.ApplicationSyntaxReference.GetSyntax(_cancellationToken) is AttributeSyntax syntax && + syntax.ArgumentList != null) + { + foreach (var arg in syntax.ArgumentList.Arguments) + { + GetDimensionDescription(arg, semanticModel, dimensionDescriptionMap); + } + } + + return (metricNameFromAttribute, dimensionHashSet, dimensionDescriptionMap); + } + + private string GetSymbolXmlCommentSummary(ISymbol symbol) + { + var xmlComment = symbol.GetDocumentationCommentXml(); + if (string.IsNullOrEmpty(xmlComment)) + { + return string.Empty; + } + + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlComment); + var summaryNode = xmlDoc.SelectSingleNode("//summary"); + if (summaryNode != null) + { + var summaryString = summaryNode.InnerXml.Trim(); + return summaryString; + } + else + { + return string.Empty; + } + } + catch (XmlException ex) + { + Diag(DiagDescriptors.ErrorXmlNotLoadedCorrectly, symbol.GetLocation(), ex.Message); + return string.Empty; + } + } + + private void GetDimensionDescription( + AttributeArgumentSyntax arg, + SemanticModel semanticModel, + Dictionary dimensionDescriptionDictionary) + { + if (arg.NameEquals != null) + { + return; + } + + var symbol = semanticModel.GetSymbolInfo(arg.Expression, _cancellationToken).Symbol; + if (symbol is not IFieldSymbol fieldSymbol || + !fieldSymbol.HasConstantValue || + fieldSymbol.ConstantValue == null) + { + return; + } + + var xmlDefinition = GetSymbolXmlCommentSummary(symbol); + if (!string.IsNullOrEmpty(xmlDefinition)) + { + dimensionDescriptionDictionary.Add(fieldSymbol.ConstantValue.ToString(), xmlDefinition); + } } private (MetricMethod? metricMethod, bool keepMethod) ProcessMethodAttribute( @@ -332,7 +405,8 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP IMethodSymbol methodSymbol, AttributeData methodAttribute, SymbolHolder symbols, - HashSet metricNames) + HashSet metricNames, + SemanticModel semanticModel) { var (instrumentKind, genericType) = GetInstrumentType(methodAttribute.AttributeClass, symbols); if (instrumentKind == InstrumentKind.None || @@ -341,6 +415,20 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP return (null, false); } + // check if any of methodSymbol parameters is of IMeter type + if (symbols.IMeterInterface != null && + methodSymbol.Parameters.Any(p => ParserUtilities.IsBaseOrIdentity(p.Type, symbols.IMeterInterface, _compilation))) + { + // The method uses old IMeter, no need to parse it + return (null, false); + } + + if (instrumentKind == InstrumentKind.Gauge) + { + Diag(DiagDescriptors.ErrorGaugeNotSupported, methodSymbol.GetLocation()); + return (null, false); + } + bool keepMethod = CheckMethodReturnType(methodSymbol); if (!_allowedGenericAttributeTypeArgs.Contains(genericType.SpecialType)) { @@ -348,11 +436,7 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP keepMethod = false; } - string metricNameFromAttribute; - HashSet dimensions; - var strongTypeDimensionConfigs = new List(); - var strongTypeObjectName = string.Empty; - var isClass = false; + var strongTypeAttrParams = new StrongTypeAttributeParameters(); if (!methodAttribute.ConstructorArguments.IsDefaultOrEmpty && methodAttribute.ConstructorArguments[0].Kind == TypedConstantKind.Type) @@ -365,14 +449,12 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP namedArg = methodAttribute.NamedArguments[0]; } - (metricNameFromAttribute, dimensions, strongTypeDimensionConfigs, strongTypeObjectName, isClass) = ExtractStrongTypeAttributeParameters( - ctorArg, - namedArg, - symbols); + strongTypeAttrParams = ExtractStrongTypeAttributeParameters(ctorArg, namedArg, symbols); } else { - (metricNameFromAttribute, dimensions) = ExtractAttributeParameters(methodAttribute); + var parameters = ExtractAttributeParameters(methodAttribute, semanticModel); + (strongTypeAttrParams.MetricNameFromAttribute, strongTypeAttrParams.DimensionHashSet, strongTypeAttrParams.DimensionDescriptionDictionary) = parameters; } string metricNameFromMethod = methodSymbol.ReturnType.Name; @@ -380,19 +462,26 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP var metricMethod = new MetricMethod { Name = methodSymbol.Name, - MetricName = string.IsNullOrWhiteSpace(metricNameFromAttribute) ? metricNameFromMethod : metricNameFromAttribute, + MetricName = string.IsNullOrWhiteSpace(strongTypeAttrParams.MetricNameFromAttribute) ? metricNameFromMethod : strongTypeAttrParams.MetricNameFromAttribute, InstrumentKind = instrumentKind, GenericType = genericType.ToDisplayString(_genericTypeSymbolFormat), - DimensionsKeys = dimensions, + DimensionsKeys = strongTypeAttrParams.DimensionHashSet, IsExtensionMethod = methodSymbol.IsExtensionMethod, Modifiers = methodSyntax.Modifiers.ToString(), MetricTypeName = methodSymbol.ReturnType.ToDisplayString(), // Roslyn doesn't know this type yet, no need to use a format here - StrongTypeConfigs = strongTypeDimensionConfigs, - StrongTypeObjectName = strongTypeObjectName, - IsDimensionTypeClass = isClass, - MetricTypeModifiers = typeDeclaration.Modifiers.ToString() + StrongTypeConfigs = strongTypeAttrParams.StrongTypeConfigs, + StrongTypeObjectName = strongTypeAttrParams.StrongTypeObjectName, + IsDimensionTypeClass = strongTypeAttrParams.IsClass, + MetricTypeModifiers = typeDeclaration.Modifiers.ToString(), + DimensionDescriptionDictionary = strongTypeAttrParams.DimensionDescriptionDictionary }; + var xmlDefinition = GetSymbolXmlCommentSummary(methodSymbol); + if (!string.IsNullOrEmpty(xmlDefinition)) + { + metricMethod.XmlDefinition = xmlDefinition; + } + if (metricMethod.Name[0] == '_') { // can't have logging method names that start with _ since that can lead to conflicting symbol names @@ -472,14 +561,6 @@ private static (string metricName, HashSet dimensions) ExtractAttributeP break; } - if (isFirstParam && - symbols.IMeterInterface != null && - ParserUtilities.IsBaseOrIdentity(paramTypeSymbol, symbols.IMeterInterface, _compilation)) - { - // The method uses old IMeter, no need to parse it - return (null, false); - } - var meterParameter = new MetricParameter { Name = paramName, @@ -547,49 +628,50 @@ private void Diag(DiagnosticDescriptor desc, Location? location, params object?[ _reportDiagnostic(Diagnostic.Create(desc, location, messageArgs)); } - private (string metricName, HashSet dimensions, List strongTypeConfigs, string strongTypeObjectName, bool isClass) - ExtractStrongTypeAttributeParameters( - TypedConstant constructorArg, - KeyValuePair namedArgument, - SymbolHolder symbols) + private StrongTypeAttributeParameters ExtractStrongTypeAttributeParameters( + TypedConstant constructorArg, + KeyValuePair namedArgument, + SymbolHolder symbols) { - var dimensionHashSet = new HashSet(); - string metricNameFromAttribute = string.Empty; - var strongTypeConfigs = new List(); - bool isClass = false; + var strongTypeAttributeParameters = new StrongTypeAttributeParameters(); if (namedArgument is { Key: "Name", Value.Value: { } }) { - metricNameFromAttribute = namedArgument.Value.Value.ToString(); + strongTypeAttributeParameters.MetricNameFromAttribute = namedArgument.Value.Value.ToString(); } if (constructorArg.IsNull || constructorArg.Value is not INamedTypeSymbol strongTypeSymbol) { - return (metricNameFromAttribute, dimensionHashSet, strongTypeConfigs, string.Empty, isClass); + return strongTypeAttributeParameters; } // Need to check if the strongType is a class or struct, classes need a null check whereas structs do not. if (strongTypeSymbol.TypeKind == TypeKind.Class) { - isClass = true; + strongTypeAttributeParameters.IsClass = true; } // Loop through all of the members of the object level and below foreach (var member in strongTypeSymbol.GetMembers()) { - strongTypeConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, symbols, _builders.GetStringBuilder())); + var dimConfigs = BuildDimensionConfigs(member, strongTypeAttributeParameters.DimensionHashSet, + strongTypeAttributeParameters.DimensionDescriptionDictionary, symbols, _builders.GetStringBuilder()); + + strongTypeAttributeParameters.StrongTypeConfigs.AddRange(dimConfigs); } // Now that all of the current level and below dimensions are extracted, let's get any parent ones - strongTypeConfigs.AddRange(GetParentDimensionConfigs(strongTypeSymbol, dimensionHashSet, symbols)); + strongTypeAttributeParameters.StrongTypeConfigs.AddRange(GetParentDimensionConfigs(strongTypeSymbol, + strongTypeAttributeParameters.DimensionHashSet, strongTypeAttributeParameters.DimensionDescriptionDictionary, symbols)); - if (strongTypeConfigs.Count > MaxDimensions) + if (strongTypeAttributeParameters.StrongTypeConfigs.Count > MaxDimensions) { Diag(DiagDescriptors.ErrorTooManyDimensions, strongTypeSymbol.Locations[0]); } - return (metricNameFromAttribute, dimensionHashSet, strongTypeConfigs, constructorArg.Value.ToString(), isClass); + strongTypeAttributeParameters.StrongTypeObjectName = constructorArg.Value.ToString(); + return strongTypeAttributeParameters; } /// @@ -603,6 +685,7 @@ private void Diag(DiagnosticDescriptor desc, Location? location, params object?[ private List BuildDimensionConfigs( ISymbol symbol, HashSet dimensionHashSet, + Dictionary dimensionDescriptionDictionary, SymbolHolder symbols, StringBuilder stringBuilder) { @@ -667,6 +750,12 @@ private List BuildDimensionConfigs( DimensionName = name, StrongTypeMetricObjectType = StrongTypeMetricObjectType.Enum }); + + var xmlDefinition = GetSymbolXmlCommentSummary(symbol); + if (!string.IsNullOrEmpty(xmlDefinition)) + { + dimensionDescriptionDictionary.Add(string.IsNullOrEmpty(dimensionName) ? symbol.Name : dimensionName, xmlDefinition); + } } return dimensionConfigs; @@ -693,6 +782,12 @@ private List BuildDimensionConfigs( DimensionName = name, StrongTypeMetricObjectType = StrongTypeMetricObjectType.String }); + + var xmlDefinition = GetSymbolXmlCommentSummary(symbol); + if (!string.IsNullOrEmpty(xmlDefinition)) + { + dimensionDescriptionDictionary.Add(string.IsNullOrEmpty(dimensionName) ? symbol.Name : dimensionName, xmlDefinition); + } } return dimensionConfigs; @@ -716,6 +811,7 @@ private List BuildDimensionConfigs( namedTypeSymbol, stringBuilder, dimensionHashSet, + dimensionDescriptionDictionary, symbols, true)); @@ -751,6 +847,7 @@ private List BuildDimensionConfigs( namedTypeSymbol, stringBuilder, dimensionHashSet, + dimensionDescriptionDictionary, symbols, false)); } @@ -774,6 +871,7 @@ private List WalkObjectModel( INamedTypeSymbol namedTypeSymbol, StringBuilder stringBuilder, HashSet dimensionHashSet, + Dictionary dimensionDescriptionDictionary, SymbolHolder symbols, bool isClass) { @@ -793,7 +891,7 @@ private List WalkObjectModel( foreach (var member in namedTypeSymbol.GetMembers()) { - dimensionConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, symbols, stringBuilder)); + dimensionConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, dimensionDescriptionDictionary, symbols, stringBuilder)); } return dimensionConfigs; @@ -802,6 +900,7 @@ private List WalkObjectModel( private List GetParentDimensionConfigs( ITypeSymbol symbol, HashSet dimensionHashSet, + Dictionary dimensionDescriptionDictionary, SymbolHolder symbols) { var dimensionConfigs = new List(); @@ -816,7 +915,7 @@ private List GetParentDimensionConfigs( foreach (var member in parentObjectBase.GetMembers()) { - dimensionConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, symbols, _builders.GetStringBuilder())); + dimensionConfigs.AddRange(BuildDimensionConfigs(member, dimensionHashSet, dimensionDescriptionDictionary, symbols, _builders.GetStringBuilder())); } parentObjectBase = parentObjectBase.BaseType; diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs b/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs index d6a82342ef3..1f88d59f6b9 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs +++ b/src/Generators/Microsoft.Gen.Metering/Common/Resources.Designer.cs @@ -78,6 +78,24 @@ internal static string ErrorDuplicateDimensionNameTitle { } } + /// + /// Looks up a localized string similar to Gauge is not supported yet by metering generator. + /// + internal static string ErrorGaugeNotSupportedMessage { + get { + return ResourceManager.GetString("ErrorGaugeNotSupportedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Gauge is not supported yet. + /// + internal static string ErrorGaugeNotSupportedTitle { + get { + return ResourceManager.GetString("ErrorGaugeNotSupportedTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to A type '{0}' cannot be used as a metering attribute type argument. /// @@ -365,5 +383,23 @@ internal static string ErrorTooManyDimensionsTitle { return ResourceManager.GetString("ErrorTooManyDimensionsTitle", resourceCulture); } } + + /// + /// Looks up a localized string similar to Xml comment was not parsed correctly, exception {0} was thrown. + /// + internal static string ErrorXmlNotLoadedCorrectlyMessage { + get { + return ResourceManager.GetString("ErrorXmlNotLoadedCorrectlyMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Xml comment was not parsed correctly. + /// + internal static string ErrorXmlNotLoadedCorrectlyTitle { + get { + return ResourceManager.GetString("ErrorXmlNotLoadedCorrectlyTitle", resourceCulture); + } + } } } diff --git a/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx b/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx index b36410945bf..9a1ec5b6ac1 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx +++ b/src/Generators/Microsoft.Gen.Metering/Common/Resources.resx @@ -123,6 +123,12 @@ A strong type object contains duplicate dimension names + + Gauge is not supported yet by metering generator + + + Gauge is not supported yet + A type '{0}' cannot be used as a metering attribute type argument @@ -219,4 +225,10 @@ A metric class contains too many dimensions - + + Xml comment was not parsed correctly + + + Xml comment was not parsed correctly, exception {0} was thrown + + \ No newline at end of file diff --git a/src/Generators/Microsoft.Gen.Metering/Common/StrongTypeAttributeParameters.cs b/src/Generators/Microsoft.Gen.Metering/Common/StrongTypeAttributeParameters.cs new file mode 100644 index 00000000000..7ba9cf38170 --- /dev/null +++ b/src/Generators/Microsoft.Gen.Metering/Common/StrongTypeAttributeParameters.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.Gen.Metering.Model; + +namespace Microsoft.Gen.Metering; + +internal sealed class StrongTypeAttributeParameters +{ + public string MetricNameFromAttribute = string.Empty; + public HashSet DimensionHashSet = new(); + public Dictionary DimensionDescriptionDictionary = new(); + public List StrongTypeConfigs = new(); + public string StrongTypeObjectName = string.Empty; + public bool IsClass; +} diff --git a/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs b/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs index 49c1d73c2da..207b934828b 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs +++ b/src/Generators/Microsoft.Gen.Metering/Common/SymbolHolder.cs @@ -11,6 +11,7 @@ internal sealed record class SymbolHolder( INamedTypeSymbol? CounterOfTAttribute, INamedTypeSymbol HistogramAttribute, INamedTypeSymbol? HistogramOfTAttribute, + INamedTypeSymbol? GaugeAttribute, INamedTypeSymbol LongTypeSymbol, INamedTypeSymbol? DimensionAttribute, INamedTypeSymbol? IMeterInterface); diff --git a/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs b/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs index b131b3a5aae..05e3572e447 100644 --- a/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs +++ b/src/Generators/Microsoft.Gen.Metering/Common/SymbolLoader.cs @@ -9,6 +9,7 @@ internal static class SymbolLoader { internal const string CounterTAttribute = "Microsoft.Extensions.Telemetry.Metering.CounterAttribute`1"; internal const string HistogramTAttribute = "Microsoft.Extensions.Telemetry.Metering.HistogramAttribute`1"; + internal const string GaugeAttribute = "Microsoft.Extensions.Telemetry.Metering.GaugeAttribute"; internal const string CounterAttribute = "Microsoft.Extensions.Telemetry.Metering.CounterAttribute"; internal const string HistogramAttribute = "Microsoft.Extensions.Telemetry.Metering.HistogramAttribute"; internal const string DimensionAttribute = "Microsoft.Extensions.Telemetry.Metering.DimensionAttribute"; @@ -20,6 +21,7 @@ internal static class SymbolLoader var meterClassSymbol = compilation.GetTypeByMetadataName(MeterClass); var counterAttribute = compilation.GetTypeByMetadataName(CounterAttribute); var histogramAttribute = compilation.GetTypeByMetadataName(HistogramAttribute); + if (meterClassSymbol == null || counterAttribute == null || histogramAttribute == null) @@ -30,6 +32,7 @@ internal static class SymbolLoader var counterTAttribute = compilation.GetTypeByMetadataName(CounterTAttribute); var histogramTAttribute = compilation.GetTypeByMetadataName(HistogramTAttribute); + var gaugeAttribute = compilation.GetTypeByMetadataName(GaugeAttribute); var dimensionAttribute = compilation.GetTypeByMetadataName(DimensionAttribute); var longType = compilation.GetSpecialType(SpecialType.System_Int64); var meterInterface = compilation.GetTypeByMetadataName(MeterInterface); @@ -40,6 +43,7 @@ internal static class SymbolLoader counterTAttribute, histogramAttribute, histogramTAttribute, + gaugeAttribute, longType, dimensionAttribute, meterInterface); diff --git a/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj b/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj index 260d679769a..a61f6d3d0c8 100644 --- a/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj +++ b/src/Generators/Microsoft.Gen.Metering/Roslyn3.8/Microsoft.Gen.Metering.Roslyn3.8.csproj @@ -11,6 +11,10 @@ 85 + + + + True diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs index 44366171327..b5750557342 100644 --- a/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs +++ b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionEmitter.cs @@ -11,169 +11,139 @@ namespace Microsoft.Gen.MeteringReports; // Stryker disable all -internal static class MetricDefinitionEmitter +internal sealed class MetricDefinitionEmitter : EmitterBase { - private static readonly StringBuilderPool _builders = new(); + internal MetricDefinitionEmitter() + : base(false) + { + } - public static string GenerateReport(IReadOnlyList metricClasses, CancellationToken cancellationToken) + public string GenerateReport(IReadOnlyList metricClasses, CancellationToken cancellationToken) { if (metricClasses == null || metricClasses.Count == 0) { return string.Empty; } - var sb = _builders.GetStringBuilder(); - try + OutLn("["); + + for (int i = 0; i < metricClasses.Count; i++) { - _ = sb.Append('[') - .Append('\n'); + cancellationToken.ThrowIfCancellationRequested(); + var metricClass = metricClasses[i]; + GenMetricClassDefinition(metricClass, cancellationToken); - for (int i = 0; i < metricClasses.Count; i++) + if (i < metricClasses.Count - 1) { - cancellationToken.ThrowIfCancellationRequested(); - var metricClass = metricClasses[i]; - _ = sb.Append(GenMetricClassDefinition(metricClass, cancellationToken)); - - if (i < metricClasses.Count - 1) - { - _ = sb.Append(','); - } - - _ = sb.Append('\n'); + Out(","); } - _ = sb.Append(']'); - - return sb.ToString(); - } - finally - { - _builders.ReturnStringBuilder(sb); + OutLn(); } + + Out("]"); + return Capture(); } - private static string GenMetricClassDefinition(ReportedMetricClass metricClass, CancellationToken cancellationToken) + private void GenMetricClassDefinition(ReportedMetricClass metricClass, CancellationToken cancellationToken) { - var sb = _builders.GetStringBuilder(); - try - { - cancellationToken.ThrowIfCancellationRequested(); - _ = sb.Append(" {") - .Append('\n'); - - _ = sb.Append($" \"{metricClass.RootNamespace}\":"); - _ = sb.Append('\n'); + cancellationToken.ThrowIfCancellationRequested(); + OutLn(" {"); - if (metricClass.Methods.Length > 0) - { - _ = sb.Append(" [") - .Append('\n'); + OutLn($" \"{metricClass.RootNamespace}\":"); - for (int j = 0; j < metricClass.Methods.Length; j++) - { - var metricMethod = metricClass.Methods[j]; + if (metricClass.Methods.Length > 0) + { + OutLn(" ["); - _ = sb.Append(GenMetricMethodDefinition(metricMethod, cancellationToken)); + for (int j = 0; j < metricClass.Methods.Length; j++) + { + var metricMethod = metricClass.Methods[j]; - if (j < metricClass.Methods.Length - 1) - { - _ = sb.Append(','); - } + GenMetricMethodDefinition(metricMethod, cancellationToken); - _ = sb.Append('\n'); + if (j < metricClass.Methods.Length - 1) + { + Out(","); } - _ = sb.Append(" ]") - .Append('\n'); + OutLn(); } - _ = sb.Append(" }"); - - return sb.ToString(); - } - finally - { - _builders.ReturnStringBuilder(sb); + OutLn(" ]"); } + + Out(" }"); } - private static string GenMetricMethodDefinition(ReportedMetricMethod metricMethod, CancellationToken cancellationToken) + private void GenMetricMethodDefinition(ReportedMetricMethod metricMethod, CancellationToken cancellationToken) { switch (metricMethod.Kind) { case InstrumentKind.Counter: case InstrumentKind.Histogram: case InstrumentKind.Gauge: - var sb = _builders.GetStringBuilder(); try { cancellationToken.ThrowIfCancellationRequested(); - _ = sb.Append(" {") - .Append('\n'); + OutLn(" {"); - _ = sb.Append($" \"MetricName\": \"{metricMethod.MetricName.Replace("\\", "\\\\").Replace("\"", "\\\"")}\",") - .Append('\n'); + OutLn($" \"MetricName\": \"{metricMethod.MetricName.Replace("\\", "\\\\").Replace("\"", "\\\"")}\","); if (!string.IsNullOrEmpty(metricMethod.Summary)) { - _ = sb.Append($" \"MetricDescription\": \"{metricMethod.Summary.Replace("\\", "\\\\").Replace("\"", "\\\"")}\","); - _ = sb.Append('\n'); + OutLn($" \"MetricDescription\": \"{metricMethod.Summary.Replace("\\", "\\\\").Replace("\"", "\\\"")}\","); } - _ = sb.Append($" \"InstrumentName\": \"{metricMethod.Kind}\""); + Out($" \"InstrumentName\": \"{metricMethod.Kind}\""); if (metricMethod.Dimensions.Count > 0) { - _ = sb.Append(','); - _ = sb.Append('\n'); - _ = sb.Append(" \"Dimensions\": {"); + OutLn(","); + + Out(" \"Dimensions\": {"); int k = 0; foreach (var dimension in metricMethod.Dimensions) { - _ = sb.Append('\n'); + OutLn(); if (metricMethod.DimensionsDescriptions.TryGetValue(dimension, out var description)) { - _ = sb.Append($" \"{dimension}\": \"{description.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""); + Out($" \"{dimension}\": \"{description.Replace("\\", "\\\\").Replace("\"", "\\\"")}\""); } else { - _ = sb.Append($" \"{dimension}\": \"\""); + Out($" \"{dimension}\": \"\""); } if (k < metricMethod.Dimensions.Count - 1) { - _ = sb.Append(','); + Out(","); } k++; } - _ = sb.Append('\n'); - _ = sb.Append(" }"); - _ = sb.Append('\n'); + OutLn(); + Out(" }"); + OutLn(); } else { - _ = sb.Append('\n'); + OutLn(); } - _ = sb.Append(" }"); - - return sb.ToString(); + Out(" }"); } catch (Exception e) { // This should report diagnostic. throw new InvalidOperationException($"An exception occurred during metric report generation {e.GetType()}:{e.Message}."); } - finally - { - _builders.ReturnStringBuilder(sb); - } + break; case InstrumentKind.None: case InstrumentKind.CounterT: case InstrumentKind.HistogramT: diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs index 408948655e1..567e39cf3c9 100644 --- a/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs +++ b/src/Generators/Microsoft.Gen.MeteringReports/Common/MetricDefinitionGenerator.cs @@ -26,8 +26,6 @@ public class MetricDefinitionGenerator : ISourceGenerator private const string CurrentProjectPath = "build_property.projectdir"; private const string FileName = "MeteringReport.json"; - private static readonly Dictionary _empty = new(); - private string? _compilationOutputPath; private string? _currentProjectPath; private string? _reportOutputPath; @@ -70,8 +68,9 @@ public void Execute(GeneratorExecutionContext context) _ = options.TryGetValue(RootNamespace, out var rootNamespace); + var emitter = new MetricDefinitionEmitter(); var reportedMetrics = MapToCommonModel(meteringClasses, rootNamespace); - var report = MetricDefinitionEmitter.GenerateReport(reportedMetrics, context.CancellationToken); + var report = emitter.GenerateReport(reportedMetrics, context.CancellationToken); #pragma warning disable R9A017 // Switch to an asynchronous metricMethod for increased performance; Cannot because it is void metricMethod, and generators dont support tasks. File.WriteAllText(Path.Combine(path, FileName), report, Encoding.UTF8); @@ -88,10 +87,10 @@ private static ReportedMetricClass[] MapToCommonModel(IReadOnlyList Modifiers: meteringClass.Modifiers, Methods: meteringClass.Methods.Select(meteringMethod => new ReportedMetricMethod( MetricName: meteringMethod.MetricName ?? "(Missing Name)", - Summary: "(Missing Summary)" /* Missing feature in .NET metering generator. */, + Summary: meteringMethod.XmlDefinition ?? "(Missing Summary)", Kind: meteringMethod.InstrumentKind, Dimensions: meteringMethod.DimensionsKeys, - DimensionsDescriptions: _empty /* Missing feature in .NET metering generator. */)) + DimensionsDescriptions: meteringMethod.DimensionDescriptionDictionary)) .ToArray())); return reportedMetrics.ToArray(); diff --git a/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props b/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props index 42e800fc305..dbc608865c3 100644 --- a/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props +++ b/src/Generators/Microsoft.Gen.MeteringReports/Directory.Build.props @@ -17,10 +17,12 @@ + + diff --git a/src/Generators/Shared/GeneratorUtilities.cs b/src/Generators/Shared/GeneratorUtilities.cs index 56c865d055b..17b9e2d92d2 100644 --- a/src/Generators/Shared/GeneratorUtilities.cs +++ b/src/Generators/Shared/GeneratorUtilities.cs @@ -125,7 +125,7 @@ static string GetAttributeDisplayName(INamedTypeSymbol attributeType) /// /// Reports will not be generated during design time to prevent file being written on every keystroke in VS. - /// Refererences: + /// References: /// 1. /// Design-time build. diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/RecordProperty.json b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/RecordProperty.json new file mode 100644 index 00000000000..fdeec987f18 --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/GoldenReports/RecordProperty.json @@ -0,0 +1,201 @@ + +{ + "Name": "test.dll", + "Types": [ + { + "Name": "Test.DerivedRecordProperty", + "Members": [ + { + "Name": "EqualityContract", + "Type": "System.Type", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C1" + } + ] + }, + { + "Name": "F3", + "Type": "int", + "File": "src-0.cs", + "Line": "17", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 1" + } + ] + }, + { + "Name": "F4", + "Type": "int", + "File": "src-0.cs", + "Line": "20", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "23", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C2", + "Notes": "Note 2" + }, + { + "Name": "C3", + "Notes": "Note 3" + }, + { + "Name": "C4" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "26", + "Classifications": [ + { + "Name": "C3" + } + ] + } + ] + }, + { + "Name": "Test.RecordProperty", + "Members": [ + { + "Name": "F0", + "Type": "string", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "F2", + "Type": "int", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C3" + } + ] + }, + { + "Name": "F3", + "Type": "int", + "File": "src-0.cs", + "Line": "17", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 1" + } + ] + }, + { + "Name": "F4", + "Type": "int", + "File": "src-0.cs", + "Line": "20", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C3", + "Notes": "Note 3" + }, + { + "Name": "C4" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "26", + "Classifications": [ + { + "Name": "C3" + } + ] + } + ], + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "29", + "Classifications": [ + { + "Name": "C3", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "29" + } + ] + }, + { + "Name": "LogWorld", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "32", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "32" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/RecordProperty.cs b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/RecordProperty.cs new file mode 100644 index 00000000000..bd205caedec --- /dev/null +++ b/test/Generators/Microsoft.Gen.ComplianceReports/TestClasses/RecordProperty.cs @@ -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 Microsoft.Extensions.Telemetry.Logging; + +namespace Test; + +interface IBar +{ + [C4] + public int P0 { get; } +} + +public record RecordProperty([C2] string F0, string F1, [C3] int F2) : IBar +{ + [C2(Notes = "Note 1")] + public int F3; + + [C2(Notes = null!)] + public int F4; + + [C3(Notes = "Note 3")] + public int P0 { get; }; + + [C3] + public int P1 { get; }; + + [LogMethod("Hello {user}")] + public void LogHello([C3(Notes = "Note 3")] string user, int port); + + [LogMethod("World {user}")] + public void LogWorld([C2] string user, int port); +} + +[C1] +public record DerivedRecordProperty : RecordProperty +{ + [C2(Notes = "Note 2")] + public override int P0 { get; }; +} diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs index 443c0a87809..e0c0c3cab62 100644 --- a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs +++ b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Common/GeneratorTests.cs @@ -91,7 +91,7 @@ public async Task TestAll() static async Task> RunGenerator(string code, string outputFile) { var (d, _) = await RoslynTestUtils.RunGenerator( - new Generator(outputFile), + new ComplianceReportsGenerator(outputFile), new[] { Assembly.GetAssembly(typeof(ILogger))!, @@ -115,7 +115,7 @@ public async Task MissingDataClassificationSymbol() const string Source = "class Nothing {}"; var (d, _) = await RoslynTestUtils.RunGenerator( - new Generator("Foo"), + new ComplianceReportsGenerator("Foo"), null, new[] { diff --git a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props index d04ec369659..6ea23785569 100644 --- a/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props +++ b/test/Generators/Microsoft.Gen.ComplianceReports/Unit/Directory.Build.props @@ -13,7 +13,7 @@ - + TestClasses\%(RecursiveDir)%(Filename)%(Extension) diff --git a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs index eec0b22fd35..f5fc3d8b87f 100644 --- a/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs +++ b/test/Generators/Microsoft.Gen.ContextualOptions/Unit/Common/EmitterTests.cs @@ -134,7 +134,7 @@ public async Task TestEmitter() } var (d, r) = await RoslynTestUtils.RunGenerator( - new Generator(), + new ContextualOptionsGenerator(), new[] { typeof(OptionsContextAttribute).Assembly, @@ -146,7 +146,7 @@ public async Task TestEmitter() Assert.Empty(d); _ = Assert.Single(r); - var golden = File.ReadAllText($"GoldenFiles/Microsoft.Gen.ContextualOptions/Microsoft.Gen.ContextualOptions.Generator/ContextualOptions.g.cs"); + var golden = File.ReadAllText($"GoldenFiles/Microsoft.Gen.ContextualOptions/Microsoft.Gen.ContextualOptions.ContextualOptionsGenerator/ContextualOptions.g.cs"); var result = r[0].SourceText.ToString(); Assert.Equal(golden, result); } diff --git a/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs index 500aafac9c8..845c7216901 100644 --- a/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs +++ b/test/Generators/Microsoft.Gen.Metering/TestClasses/MetricConstants.cs @@ -10,5 +10,10 @@ internal static class MetricConstants public const string D2 = "Dim_2"; // dots are not supported in dimension names public const string D3 = "Dim_3"; // dashes are not supported in dimension names + + /// + /// Dim4 description. + /// + public const string DimWithXmlComment = "Dim4"; } } diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs index 7dfc6664b83..62de5db40a3 100644 --- a/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/EmitterTests.cs @@ -33,7 +33,7 @@ public async Task TestEmitter() } var (d, r) = await RoslynTestUtils.RunGenerator( - new Generator(), + new MeteringGenerator(), new[] { Assembly.GetAssembly(typeof(Meter))!, @@ -48,7 +48,7 @@ public async Task TestEmitter() Assert.Empty(d); Assert.Equal(2, r.Length); - string generatedContentPath = "GoldenFiles/Microsoft.Gen.Metering/Microsoft.Gen.Metering.Generator"; + string generatedContentPath = "GoldenFiles/Microsoft.Gen.Metering/Microsoft.Gen.Metering.MeteringGenerator"; var goldenCache = File.ReadAllText($"{generatedContentPath}/Factory.g.cs"); var goldenMetrics = File.ReadAllText($"{generatedContentPath}/Metering.g.cs"); diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs index 903f3333dd5..31af715af15 100644 --- a/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.StrongTypes.cs @@ -196,7 +196,8 @@ public static partial class MetricClass { [Counter(typeof(CounterStruct), Name=""TotalCountTest"")] public static partial TotalCount CreateTotalCountCounter(Meter meter); - }"); + } + "); Assert.Empty(d); } @@ -291,7 +292,8 @@ public static partial class MetricClass public static partial TotalCount CreateTotalCountCounter(Meter meter); }"); - Assert.Empty(d); + Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorGaugeNotSupported.Id, d[0].Id); } [Fact] @@ -356,6 +358,332 @@ public static partial class MetricClass public static partial TotalCount CreateTotalCountCounter(Meter meter); }"); + Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorGaugeNotSupported.Id, d[0].Id); + } + + [Fact] + public async Task StrongTypeCounterWithDescription() + { + var d = await RunGenerator(@" + internal static partial class Metric + { + /// + /// Dimension1 description. + /// + public const string Dim1 = ""Dim1""; + + /// + /// DescribedDimensionCounter description. + /// + /// + /// + [Counter(DescripedDimensions.Dimension1, Dim1)] + public static partial DescribedDimensionCounter CreatePublicCounter(Meter meter); + + /// + /// DimenisonDefinedInMetricClass description. + /// + public const string DimenisonDefinedInMetricClass = ""DimenisonDefinedInMetricClass""; + + /// + /// DescribedDimensionHistogram description. + /// + /// + /// + [Histogram(DescripedDimensions.Dimension2, DimenisonDefinedInMetricClass)] + public static partial DescribedDimensionHistogram CreatePublicHistogram(Meter meter); + + /// + /// StrongTypeCounterWithDescripedDimension description. + /// + /// + /// + [Counter(typeof(DimensionForStrongTypes), Name = ""MyStrongTypeMetricWithDescription"")] + public static partial StrongTypeCounterWithDescripedDimension CreateStrongTypeCounterWithDescibedDimensions(Meter meter); + } + + /// + /// DescripedDimensions class description. + /// + internal static class DescripedDimensions + { + /// + /// Dimension1 in class description. + /// + public const string Dimension1 = ""Dimension1""; + + /// + /// Dimension2 description. + /// + public const string Dimension2 = ""Dimension2""; + + /// + /// Dimension3 description. + /// + public const string Dimension3 = ""Dimension3""; + } + + public class DimensionForStrongTypes + { + /// + /// Gets or sets anotherDimension. + /// + public string? AnotherDimension { get; set; } + + /// + /// Gets or sets MetricEnum. + /// + public MetricOperations MetricEnum { get; set; } + + /// + /// Gets or sets MetricEnum2. + /// + [Dimension(""Enum2"")] + public MetricOperations MetricEnum2 { get; set; } + + /// + /// Gets or sets ChildDimensionsClass. + /// + public ChildClassDimensionForStrongTypes? ChildDimensionsClass { get; set; } + + /// + /// Gets or sets ChildDimensionsStruct. + /// + public DimensionForStrongTypesDimensionsStruct ChildDimensionsStruct { get; set; } + } + + public enum MetricOperations + { + Unknown = 0, + Operation1 = 1, + } + + public class ChildClassDimensionForStrongTypes + { + /// + /// Gets or sets Dim2. + /// + public string? Dim2 { get; set; } + + /// + /// Gets or sets SomeDim. + /// + [Dimension(""dim2FromAttribute"")] + public string? SomeDim; + } + + public struct DimensionForStrongTypesDimensionsStruct + { + /// + /// Gets or sets Dim4Struct. + /// + public string Dim4Struct { get; set; } + + /// + /// Gets or sets Dim5Struct. + /// + [Dimension(""Dim5FromAttribute"")] + public string Dim5Struct { get; set; } + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StrongTypeHistogramWithDescription() + { + // This test should return no errors. + var d = await RunGenerator(@" + public class DimensionsTest : ParentDimensions + { + /// + /// test1 description. + /// + public string? test1 { get; set; } + + /// + /// test1_FromAttribute description. + /// + [Dimension(""test1_FromAttribute"")] + public string? test1_WithAttribute { get; set; } + + /// + /// operations_FromAttribute description. + /// + [Dimension(""operations_FromAttribute"")] + public Operations operations {get;set;} + + /// + /// ChildDimensions1 description. + /// + public ChildDimensions? ChildDimensions1 { get; set; } + + public void Method() + { + System.Console.WriteLine(""I am a method.""); + } + } + + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public class ParentDimensions + { + /// + /// parentDimension_FromAttribute description. + /// + [Dimension(""parentDimension_FromAttribute"")] + public string? ParentOperationNameWithAttribute { get;set; } + + /// + /// ParentOperationName description. + /// + public string? ParentOperationName { get;set; } + + public DimensionsStruct ChildDimensionsStruct { get; set; } + } + + public class ChildDimensions + { + /// + /// test2_WithAttribute description. + /// + [Dimension(""test2_FromAttribute"")] + public string test2_WithAttribute { get; set; } + + /// + /// test2 description. + /// + public string test2 { get; set; } + + /// + /// test1_FromAttribute_In_Child1 description. + /// + [Dimension(""test1_FromAttribute_In_Child1"")] + public string? test1 { get; set; } + + public ChildDimensions2? ChildDimensions2 { get; set;} + } + + public class ChildDimensions2 + { + /// + /// test3_FromAttribute description. + /// + [Dimension(""test3_FromAttribute"")] + public string test3_WithAttribute { get; set; } + + /// + /// test3 description. + /// + public string test3 { get; set; } + + /// + /// test1_FromAttribute_In_Child2 description. + /// + [Dimension(""test1_FromAttribute_In_Child2"")] + public string? test1 { get; set; } + } + + public struct DimensionsStruct + { + /// + /// testStruct_WithAttribute description. + /// + [Dimension(""testStruct_FromAttribute"")] + public string testStruct_WithAttribute { get; set; } + + /// + /// testStruct description. + /// + public string testStruct { get; set; } + } + + public static partial class MetricClass + { + [Histogram(typeof(DimensionsTest), Name=""TotalCountTest"")] + public static partial TestHistogram CreateTestHistogram(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task StructTypeCounterWithDescription() + { + var d = await RunGenerator(@" + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public struct CounterStruct + { + /// + /// Dim1_FromAttribute description. + /// + [Dimension(""Dim1_FromAttribute"")] + public string? Dim1 { get; set; } + + /// + /// Dim2_FromAttribute description. + /// + [Dimension(""Dim2_FromAttribute"")] + public string? Dim2; + + /// + /// Operations_FromAttribute description. + /// + [Dimension(""Operations_FromAttribute"")] + public Operations Operations { get; set; } + } + + public static partial class MetricClass + { + [Counter(typeof(CounterStruct), Name=""TotalCountTest"")] + public static partial TotalCount CreateTotalCountCounter(Meter meter); + } + "); + + Assert.Empty(d); + } + + [Fact] + public async Task StructTypeHistogramWithDescription() + { + var d = await RunGenerator(@" + public enum Operations + { + Unknown = 0, + Operation1 = 1, + } + + public struct HistogramStruct + { + /// + /// Dim1_FromAttribute description. + /// + [Dimension(""Dim1_FromAttribute"")] + public string? Dim1 { get; set; } + + /// + /// Operations_FromAttribute description. + /// + [Dimension(""Operations_FromAttribute"")] + public Operations Operations { get; set; } + } + + public static partial class MetricClass + { + [Histogram(typeof(HistogramStruct), Name=""TotalCountTest"")] + public static partial TotalHistogram CreateTotalHistogram(Meter meter); + }"); + Assert.Empty(d); } diff --git a/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs index 16705ad5ad0..1ca8c3dcf81 100644 --- a/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs +++ b/test/Generators/Microsoft.Gen.Metering/Unit/Common/ParserTests.cs @@ -1,5 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) Microsoft Corporation. All Rights Reserved. using System; using System.Collections.Generic; @@ -606,6 +605,146 @@ partial class C Assert.Empty(d); } + [Fact] + public async Task GaugeNotSupported() + { + var d = await RunGenerator(@" + partial class C + { + [Gauge(""d1"")] + static partial NotSupportedGauge CreateGauge(Meter meter); + }"); + + _ = Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorGaugeNotSupported.Id, d[0].Id); + } + + [Fact] + public async Task DimensionIsDocumentedCounter() + { + var d = await RunGenerator(@" + partial class C + { + /// + /// InClassDim description. + /// + private const string InClassDimensionName = ""InClassDim""; + [Counter(InClassDimensionName)] + static partial TestCounter CreateTestCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task DimensionIsDocumentedHistogram() + { + var d = await RunGenerator(@" + partial class C + { + /// + /// InClassDim description. + /// + private const string InClassDimensionName = ""InClassDim""; + [Histogram(InClassDimensionName)] + static partial TestHistogram CreateTestHistogram(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task CounterIsDocumented() + { + var d = await RunGenerator(@" + partial class C + { + /// + /// TestCounter description. + /// + [Counter] + static partial TestCounter CreateTestCounter(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task HistogramIsDocumented() + { + var d = await RunGenerator(@" + partial class C + { + /// + /// TestHistogram description. + /// + [Histogram] + static partial TestHistogram CreateTestHistogram(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task CounterIsNotProperlyDocumented() + { + var d = await RunGenerator(@" + partial class C + { + /// + /// TestCounter description. + /// < /summary> + [Counter] + static partial TestCounter CreateTestCounter(Meter meter); + }"); + + Assert.Single(d); + Assert.Equal(DiagDescriptors.ErrorXmlNotLoadedCorrectly.Id, d[0].Id); + } + + [Fact] + public async Task HistogramIsNotXmlDocumented() + { + var d = await RunGenerator(@" + partial class C + { + /// no xml tags. + [Histogram] + static partial TestHistogram CreateTestHistogram(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task HistogramHasNoSummaryInXmlComment() + { + var d = await RunGenerator(@" + partial class C + { + /// + /// TestHistogram remarks. + /// + [Histogram] + static partial TestHistogram CreateTestHistogram(Meter meter); + }"); + + Assert.Empty(d); + } + + [Fact] + public async Task IMeterTypeParameter() + { + var d = await RunGenerator(@" + partial class C + { + [Histogram] + static partial TestHistogram CreateTestHistogram(IMeter meter); + }"); + + Assert.Empty(d); + } + private static async Task> RunGenerator( string code, bool wrap = true, @@ -647,7 +786,7 @@ private static async Task> RunGenerator( } var (d, _) = await RoslynTestUtils.RunGenerator( - new Generator(), + new MeteringGenerator(), refs, new[] { text }, includeBaseReferences: includeBaseReferences, diff --git a/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/IMeterAttributedWithXmlDescriptions.json b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/IMeterAttributedWithXmlDescriptions.json new file mode 100644 index 00000000000..61568ca58a3 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/IMeterAttributedWithXmlDescriptions.json @@ -0,0 +1,37 @@ +[ + { + "Test.TestClasses.MetricDefinition": + [ + { + "MetricName": "CounterWithDescription", + "MetricDescription": "CounterWithDescription description.", + "InstrumentName": "Counter" + }, + { + "MetricName": "HistogramWithDescription", + "MetricDescription": "HistogramWithDescription description.", + "InstrumentName": "Histogram" + }, + { + "MetricName": "GaugeWithDescription", + "MetricDescription": "GaugeWithDescription description.", + "InstrumentName": "Gauge" + }, + { + "MetricName": "GaugeWithWrongDescription", + "MetricDescription": "GaugeWithWrongDescription description.", + "InstrumentName": "Gauge" + }, + { + "MetricName": "HistogramWithWrongDescription", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Histogram" + }, + { + "MetricName": "CounterWithWrongDescription", + "MetricDescription": "CounterWithDescription description.", + "InstrumentName": "Counter" + } + ] + } +] \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/IMeterDimensionsAttributedWithXmlDescriptions.json b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/IMeterDimensionsAttributedWithXmlDescriptions.json new file mode 100644 index 00000000000..2e6ce70d2ca --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/IMeterDimensionsAttributedWithXmlDescriptions.json @@ -0,0 +1,47 @@ +[ + { + "Test.TestClasses.MetricDefinition": + [ + { + "MetricName": "DescribedDimensionCounter", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "Dimension1": "Dimension1 description.", + "Dim1": "" + } + }, + { + "MetricName": "DescribedDimensionHistogram", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Histogram", + "Dimensions": { + "Dimension2": "Dimension2 description." + } + }, + { + "MetricName": "DescribedDimensionGauge", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Gauge", + "Dimensions": { + "Dimension3": "Dimension3 description.", + "DimenisonDefinedInMetricClass": "DimenisonDefinedInMetricClass description." + } + }, + { + "MetricName": "MyStrongTypeMetricWithDescription", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "AnotherDimension": "Gets or sets anotherDimension.", + "MetricEnum": "Gets or sets MetricEnum.", + "Enum2": "Gets or sets MetricEnum2.", + "Dim2": "Gets or sets Dim2.", + "dim2FromAttribute": "Gets or sets SomeDim.", + "Dim4Struct": "Gets or sets Dim4Struct.", + "Dim5FromAttribute": "Gets or sets Dim5Struct." + } + } + ] + } +] \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/MeterAttributedWithXmlDescriptions.json b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/MeterAttributedWithXmlDescriptions.json new file mode 100644 index 00000000000..a8def13f363 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/MeterAttributedWithXmlDescriptions.json @@ -0,0 +1,31 @@ +[ + { + "Test.TestClasses.MetricDefinition": + [ + { + "MetricName": "CounterWithDescription", + "MetricDescription": "CounterWithDescription description.", + "InstrumentName": "Counter" + }, + { + "MetricName": "HistogramWithDescription", + "MetricDescription": "HistogramWithDescription description.", + "InstrumentName": "Histogram" + }, + { + "MetricName": "HistogramWithWrongDescription", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Histogram" + }, + { + "MetricName": "ConstDescribedCounter", + "MetricDescription": "CreateConstDescribedCounter description.", + "InstrumentName": "Counter", + "Dimensions": { + "Dim4": "Dim4 description.", + "InClassDim": "InClassDim description." + } + } + ] + } +] \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions.json b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions.json new file mode 100644 index 00000000000..ecdba6a38fa --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions.json @@ -0,0 +1,39 @@ +[ + { + "Test.TestClasses.MetricDefinition": + [ + { + "MetricName": "DescribedDimensionCounter", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "Dimension1": "Dimension1 description.", + "Dim1": "" + } + }, + { + "MetricName": "DescribedDimensionHistogram", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Histogram", + "Dimensions": { + "Dimension2": "Dimension2 description.", + "DimenisonDefinedInMetricClass": "DimenisonDefinedInMetricClass description." + } + }, + { + "MetricName": "MyStrongTypeMetricWithDescription", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "AnotherDimension": "Gets or sets anotherDimension.", + "MetricEnum": "Gets or sets MetricEnum.", + "Enum2": "Gets or sets MetricEnum2.", + "Dim2": "Gets or sets Dim2.", + "dim2FromAttribute": "Gets or sets SomeDim.", + "Dim4Struct": "Gets or sets Dim4Struct.", + "Dim5FromAttribute": "Gets or sets Dim5Struct." + } + } + ] + } +] \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/IMeterAttributedWithXmlDescriptions.cs b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/IMeterAttributedWithXmlDescriptions.cs new file mode 100644 index 00000000000..bdcf84ab562 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/IMeterAttributedWithXmlDescriptions.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes with description for metrics.")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + internal static partial class IMeterAttributedWithXmlDescriptions + { + /// + /// CounterWithDescription description. + /// + /// + /// + [Counter] + public static partial CounterWithDescription CreateDescribedCounter(IMeter meter); + + /// + /// HistogramWithDescription description. + /// + /// + /// + [Histogram] + public static partial HistogramWithDescription CreateDescribedHistogram(IMeter meter); + + /// + /// GaugeWithDescription description. + /// + /// + /// + [Gauge] + public static partial GaugeWithDescription CreateDescribedGauge(IMeter meter); + + /// + /// GaugeWithWrongDescription description. + /// + /// + /// + [Gauge] + public static partial GaugeWithWrongDescription CreateWrongDescribedGauge(IMeter meter); + + /// no xml tags + [Histogram] + public static partial HistogramWithWrongDescription CreateWrongDescribedHistogram(IMeter meter); + + /// + /// CounterWithDescription description. + /// + /// + /// + [Counter] + public static partial CounterWithWrongDescription CreateWrongDescribedCounter(IMeter meter); + } +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/IMeterDimensionsAttributedWithXmlDescriptions.cs b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/IMeterDimensionsAttributedWithXmlDescriptions.cs new file mode 100644 index 00000000000..3c6c8911b70 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/IMeterDimensionsAttributedWithXmlDescriptions.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes with description for metrics.")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + internal static partial class IMeterDimensionsAttributedWithXmlDescriptions + { + public const string Dim1 = "Dim1"; + + [Counter(DescripedDimensions.Dimension1, Dim1)] + public static partial DescribedDimensionCounter CreatePublicCounter(IMeter meter); + + [Histogram(DescripedDimensions.Dimension2)] + public static partial DescribedDimensionHistogram CreatePublicHistogram(IMeter meter); + + /// + /// DimenisonDefinedInMetricClass description. + /// + public const string DimenisonDefinedInMetricClass = "DimenisonDefinedInMetricClass"; + + [Gauge(DescripedDimensions.Dimension3, DimenisonDefinedInMetricClass)] + public static partial DescribedDimensionGauge CreatePublicGauge(IMeter meter); + + [Counter(typeof(DimensionForStrongTypes), Name = "MyStrongTypeMetricWithDescription")] + public static partial StrongTypeCounterWithDescripedDimension CreateStrongTypeCounterWithDescibedDimensions(IMeter meter); + } + +#pragma warning disable SA1402 // File may only contain a single type + + /// + /// DescripedDimensions class description. + /// + internal static class DescripedDimensions + { + /// + /// Dimension1 description. + /// + public const string Dimension1 = "Dimension1"; + + /// + /// Dimension2 description. + /// + public const string Dimension2 = "Dimension2"; + + /// + /// Dimension3 description. + /// + public const string Dimension3 = "Dimension3"; + } + + public class DimensionForStrongTypes + { + /// + /// Gets or sets anotherDimension. + /// + public string? AnotherDimension { get; set; } + + /// + /// Gets or sets MetricEnum. + /// + public MetricOperations MetricEnum { get; set; } + + /// + /// Gets or sets MetricEnum2. + /// + [Dimension("Enum2")] + public MetricOperations MetricEnum2 { get; set; } + + /// + /// Gets or sets ChildDimensionsClass. + /// + public ChildClassDimensionForStrongTypes? ChildDimensionsClass { get; set; } + + /// + /// Gets or sets ChildDimensionsStruct. + /// + public DimensionForStrongTypesDimensionsStruct ChildDimensionsStruct { get; set; } + } + + public enum MetricOperations + { + Unknown = 0, + Operation1 = 1, + } + + public class ChildClassDimensionForStrongTypes + { + /// + /// Gets or sets Dim2. + /// + public string? Dim2 { get; set; } + + /// + /// Gets or sets SomeDim. + /// + [Dimension("dim2FromAttribute")] + public string? SomeDim; + } + + public struct DimensionForStrongTypesDimensionsStruct + { + /// + /// Gets or sets Dim4Struct. + /// + public string Dim4Struct { get; set; } + + /// + /// Gets or sets Dim5Struct. + /// + [Dimension("Dim5FromAttribute")] + public string Dim5Struct { get; set; } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/MeterAttributedWithXmlDescriptions.cs b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/MeterAttributedWithXmlDescriptions.cs new file mode 100644 index 00000000000..59e54b22359 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/MeterAttributedWithXmlDescriptions.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "For testing emitter for classes with description for metrics.")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", Justification = "Metering generator tests")] + internal static partial class MeterAttributedWithXmlDescriptions + { + /// + /// InClassDim description. + /// + private const string InClassDimensionName = "InClassDim"; + + /// + /// CounterWithDescription description. + /// + /// + /// + [Counter] + public static partial CounterWithDescription CreateDescribedCounter(Meter meter); + + /// + /// HistogramWithDescription description. + /// + /// + /// + [Histogram] + public static partial HistogramWithDescription CreateDescribedHistogram(Meter meter); + + /// no xml tags + [Histogram] + public static partial HistogramWithWrongDescription CreateWrongDescribedHistogram(Meter meter); + + /// + /// CreateConstDescribedCounter description. + /// + /// + /// + [Counter(MetricConstants.DimWithXmlComment, InClassDimensionName)] + public static partial ConstDescribedCounter CreateConstDescribedCounter(Meter meter); + } + + internal static class MetricConstants + { + /// + /// Dim4 description. + /// + public const string DimWithXmlComment = "Dim4"; + } +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/MeterDimensionsAttributedWithXmlDescriptions.cs b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/MeterDimensionsAttributedWithXmlDescriptions.cs new file mode 100644 index 00000000000..f9029ee3b62 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/TestClasses/MeterDimensionsAttributedWithXmlDescriptions.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Telemetry.Metering; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes with description for metrics.")] + [SuppressMessage("Readability", "R9A046:Source generated metrics (fast metrics) should be located in 'Metric' class", + Justification = "Metering generator tests")] + internal static partial class MeterDimensionsAttributedWithXmlDescriptions + { + public const string Dim1 = "Dim1"; + + [Counter(DescripedDimensions.Dimension1, Dim1)] + public static partial DescribedDimensionCounter CreatePublicCounter(Meter meter); + + /// + /// DimenisonDefinedInMetricClass description. + /// + public const string DimenisonDefinedInMetricClass = "DimenisonDefinedInMetricClass"; + + [Histogram(DescripedDimensions.Dimension2, DimenisonDefinedInMetricClass)] + public static partial DescribedDimensionHistogram CreatePublicHistogram(Meter meter); + + [Counter(typeof(DimensionForStrongTypes), Name = "MyStrongTypeMetricWithDescription")] + public static partial StrongTypeCounterWithDescripedDimension CreateStrongTypeCounterWithDescibedDimensions(Meter meter); + } + +#pragma warning disable SA1402 // File may only contain a single type + + /// + /// DescripedDimensions class description. + /// + internal static class DescripedDimensions + { + /// + /// Dimension1 description. + /// + public const string Dimension1 = "Dimension1"; + + /// + /// Dimension2 description. + /// + public const string Dimension2 = "Dimension2"; + + /// + /// Dimension3 description. + /// + public const string Dimension3 = "Dimension3"; + } + + public class DimensionForStrongTypes + { + /// + /// Gets or sets anotherDimension. + /// + public string? AnotherDimension { get; set; } + + /// + /// Gets or sets MetricEnum. + /// + public MetricOperations MetricEnum { get; set; } + + /// + /// Gets or sets MetricEnum2. + /// + [Dimension("Enum2")] + public MetricOperations MetricEnum2 { get; set; } + + /// + /// Gets or sets ChildDimensionsClass. + /// + public ChildClassDimensionForStrongTypes? ChildDimensionsClass { get; set; } + + /// + /// Gets or sets ChildDimensionsStruct. + /// + public DimensionForStrongTypesDimensionsStruct ChildDimensionsStruct { get; set; } + } + + public enum MetricOperations + { + Unknown = 0, + Operation1 = 1, + } + + public class ChildClassDimensionForStrongTypes + { + /// + /// Gets or sets Dim2. + /// + public string? Dim2 { get; set; } + + /// + /// Gets or sets SomeDim. + /// + [Dimension("dim2FromAttribute")] + public string? SomeDim; + } + + public struct DimensionForStrongTypesDimensionsStruct + { + /// + /// Gets or sets Dim4Struct. + /// + public string Dim4Struct { get; set; } + + /// + /// Gets or sets Dim5Struct. + /// + [Dimension("Dim5FromAttribute")] + public string Dim5Struct { get; set; } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs index 1b75cdeeedf..9cd5038deee 100644 --- a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/EmitterTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using Microsoft.Gen.Metering.Model; +using Microsoft.Gen.MeteringReports; using Xunit; namespace Microsoft.Gen.MeteringReports.Test; @@ -80,26 +81,32 @@ public class EmitterTests [Fact] public void EmitterShouldThrowExceptionUponCancellation() { - Assert.Throws(() => MetricDefinitionEmitter.GenerateReport(_metricClasses, new CancellationToken(true))); + Assert.Throws(() => + { + var emitter = new MetricDefinitionEmitter(); + emitter.GenerateReport(_metricClasses, new CancellationToken(true)); + }); } [Fact] public void EmitterShouldOutputEmptyForNullInput() { - Assert.Equal(string.Empty, MetricDefinitionEmitter.GenerateReport(null!, CancellationToken.None)); + var emitter = new MetricDefinitionEmitter(); + Assert.Equal(string.Empty, emitter.GenerateReport(null!, CancellationToken.None)); } [Fact] public void EmitterShouldOutputEmptyForEmptyInputForMetricClass() { - Assert.Equal(string.Empty, MetricDefinitionEmitter.GenerateReport(Array.Empty(), CancellationToken.None)); + var emitter = new MetricDefinitionEmitter(); + Assert.Equal(string.Empty, emitter.GenerateReport(Array.Empty(), CancellationToken.None)); } [Fact] public void GetMetricClassDefinition_GivenMetricTypeIsUnknown_ThrowsNotSupportedException() { const int UnknownMetricType = 10; - + var emitter = new MetricDefinitionEmitter(); var metricClass = new ReportedMetricClass { Name = "Test", @@ -115,62 +122,64 @@ public void GetMetricClassDefinition_GivenMetricTypeIsUnknown_ThrowsNotSupported } }; - Assert.Throws(() => MetricDefinitionEmitter.GenerateReport(new[] { metricClass }, CancellationToken.None)); + Assert.Throws(() => emitter.GenerateReport(new[] { metricClass }, CancellationToken.None)); } [Fact] public void EmitterShouldOutputInJSONFormat() { - const string Expected = + string newLine = Environment.NewLine; + string expected = "[" + - "\n {" + - "\n \"MetricContainingAssembly1\":" + - "\n [" + - "\n {" + - "\n \"MetricName\": \"Requests\"," + - "\n \"MetricDescription\": \"Requests summary.\"," + - "\n \"InstrumentName\": \"Counter\"," + - "\n \"Dimensions\": {" + - "\n \"StatusCode\": \"Status code for request.\"," + - "\n \"ErrorCode\": \"Error code for request.\"" + - "\n }" + - "\n }," + - "\n {" + - "\n \"MetricName\": \"Latency\"," + - "\n \"MetricDescription\": \"Latency summary.\"," + - "\n \"InstrumentName\": \"Histogram\"," + - "\n \"Dimensions\": {" + - "\n \"Dim1\": \"\"" + - "\n }" + - "\n }," + - "\n {" + - "\n \"MetricName\": \"MemoryUsage\"," + - "\n \"InstrumentName\": \"Gauge\"" + - "\n }" + - "\n ]" + - "\n }," + - "\n {" + - "\n \"MetricContainingAssembly2\":" + - "\n [" + - "\n {" + - "\n \"MetricName\": \"Counter\"," + - "\n \"MetricDescription\": \"Counter summary.\"," + - "\n \"InstrumentName\": \"Counter\"" + - "\n }," + - "\n {" + - "\n \"MetricName\": \"R9\\\\Test\\\\MemoryUsage\"," + - "\n \"MetricDescription\": \"MemoryUsage summary.\"," + - "\n \"InstrumentName\": \"Gauge\"," + - "\n \"Dimensions\": {" + - "\n \"Path\": \"R9\\\\Test\\\\Description\\\\Path\"" + - "\n }" + - "\n }" + - "\n ]" + - "\n }" + - "\n]"; + newLine + " {" + + newLine + " \"MetricContainingAssembly1\":" + + newLine + " [" + + newLine + " {" + + newLine + " \"MetricName\": \"Requests\"," + + newLine + " \"MetricDescription\": \"Requests summary.\"," + + newLine + " \"InstrumentName\": \"Counter\"," + + newLine + " \"Dimensions\": {" + + newLine + " \"StatusCode\": \"Status code for request.\"," + + newLine + " \"ErrorCode\": \"Error code for request.\"" + + newLine + " }" + + newLine + " }," + + newLine + " {" + + newLine + " \"MetricName\": \"Latency\"," + + newLine + " \"MetricDescription\": \"Latency summary.\"," + + newLine + " \"InstrumentName\": \"Histogram\"," + + newLine + " \"Dimensions\": {" + + newLine + " \"Dim1\": \"\"" + + newLine + " }" + + newLine + " }," + + newLine + " {" + + newLine + " \"MetricName\": \"MemoryUsage\"," + + newLine + " \"InstrumentName\": \"Gauge\"" + + newLine + " }" + + newLine + " ]" + + newLine + " }," + + newLine + " {" + + newLine + " \"MetricContainingAssembly2\":" + + newLine + " [" + + newLine + " {" + + newLine + " \"MetricName\": \"Counter\"," + + newLine + " \"MetricDescription\": \"Counter summary.\"," + + newLine + " \"InstrumentName\": \"Counter\"" + + newLine + " }," + + newLine + " {" + + newLine + " \"MetricName\": \"R9\\\\Test\\\\MemoryUsage\"," + + newLine + " \"MetricDescription\": \"MemoryUsage summary.\"," + + newLine + " \"InstrumentName\": \"Gauge\"," + + newLine + " \"Dimensions\": {" + + newLine + " \"Path\": \"R9\\\\Test\\\\Description\\\\Path\"" + + newLine + " }" + + newLine + " }" + + newLine + " ]" + + newLine + " }" + + newLine + "]"; - string json = MetricDefinitionEmitter.GenerateReport(_metricClasses, CancellationToken.None); + var emitter = new MetricDefinitionEmitter(); + string json = emitter.GenerateReport(_metricClasses, CancellationToken.None); - Assert.Equal(Expected, json); + Assert.Equal(expected, json); } } diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs index b2320877515..b27d6833b7b 100644 --- a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/GeneratorTests.cs @@ -6,9 +6,6 @@ namespace Microsoft.Gen.MeteringReports.Test; -/// -/// Test for . -/// public class GeneratorTests { [Fact] diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/ParserTests.cs b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/ParserTests.cs new file mode 100644 index 00000000000..d34624ac267 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Common/ParserTests.cs @@ -0,0 +1,120 @@ +// 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.Collections.Generic; +using System.Diagnostics.Metrics; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Telemetry.Metering; +using Microsoft.Gen.Shared; +using Xunit; + +namespace Microsoft.Gen.MeteringReports.Test; + +public class ParserTests +{ + [Fact] + public async Task TestAll() + { + foreach (var inputFile in Directory.GetFiles("TestClasses")) + { + var stem = Path.GetFileNameWithoutExtension(inputFile); + var goldenReportFile = $"GoldenReports/{stem}.json"; + + var tmp = Path.Combine(Directory.GetCurrentDirectory(), "MeteringReport.json"); + var d = await RunGenerator(File.ReadAllText(inputFile)); + Assert.Empty(d); + + var golden = File.ReadAllText(goldenReportFile).ReplaceLineEndings(); + var generated = File.ReadAllText(tmp).ReplaceLineEndings(); + + Assert.Equal(golden, generated); + File.Delete(tmp); + } + } + + private static async Task> RunGenerator( + string code, + bool wrap = true, + bool inNamespace = true, + bool includeBaseReferences = true, + bool includeMeterReferences = true, + CancellationToken cancellationToken = default) + { + var text = code; + if (wrap) + { + var nspaceStart = "namespace Test {"; + var nspaceEnd = "}"; + if (!inNamespace) + { + nspaceStart = ""; + nspaceEnd = ""; + } + + text = $@" + {nspaceStart} + using Microsoft.R9.Extensions.Metering; + using System.Diagnostics.Metrics; + {code} + {nspaceEnd} + "; + } + + Assembly[]? refs = null; + if (includeMeterReferences) + { + refs = new[] + { + Assembly.GetAssembly(typeof(Meter))!, + Assembly.GetAssembly(typeof(CounterAttribute))!, + Assembly.GetAssembly(typeof(HistogramAttribute))!, + Assembly.GetAssembly(typeof(GaugeAttribute))!, + }; + } + + var (d, _) = await RoslynTestUtils.RunGenerator( + new MetricDefinitionGenerator(), + refs, + new[] { text }, + new OptionsProvider(), + includeBaseReferences: includeBaseReferences, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return d; + } + + private sealed class Options : AnalyzerConfigOptions + { + public override bool TryGetValue(string key, out string value) + { + if (key == "build_property.GenerateMeteringReport") + { + value = bool.TrueString; + return true; + } + + if (key == "build_property.MeteringReportOutputPath") + { + value = Directory.GetCurrentDirectory(); + return true; + } + + value = null!; + return false; + } + } + + private sealed class OptionsProvider : AnalyzerConfigOptionsProvider + { + public override AnalyzerConfigOptions GlobalOptions => new Options(); + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotSupportedException(); + public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => throw new NotSupportedException(); + } +} diff --git a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props index 1309376fe5d..3f9c4163761 100644 --- a/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props +++ b/test/Generators/Microsoft.Gen.MeteringReports/Unit/Directory.Build.props @@ -14,10 +14,21 @@ + + + TestClasses\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + GoldenReports\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + +