diff --git a/src/Generators/Microsoft.Gen.ComplianceReports/Emitter.cs b/src/Generators/Microsoft.Gen.ComplianceReports/Emitter.cs index 999f8f39d03..2e8b63dcae7 100644 --- a/src/Generators/Microsoft.Gen.ComplianceReports/Emitter.cs +++ b/src/Generators/Microsoft.Gen.ComplianceReports/Emitter.cs @@ -21,11 +21,15 @@ public Emitter() } [SuppressMessage("Performance", "LA0002:Use 'Microsoft.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance", Justification = "Can't use that in a generator")] - public string Emit(IReadOnlyCollection classifiedTypes, string assemblyName) + public string Emit(IReadOnlyCollection classifiedTypes, string assemblyName, bool includeName = true) // show or hide assemblyName in the report,defaulted to true. { OutObject(() => { - OutNameValue("Name", assemblyName); + // this is only for not displaying a name as part of ComplianceReport properties,it should be at the root of the report, defaulted to true for beackward compatibility + if (includeName) + { + OutNameValue("Name", assemblyName); + } OutArray("Types", () => { diff --git a/src/Generators/Microsoft.Gen.MetadataExtractor/MetadataReportsGenerator.cs b/src/Generators/Microsoft.Gen.MetadataExtractor/MetadataReportsGenerator.cs new file mode 100644 index 00000000000..8471e9d1cc3 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MetadataExtractor/MetadataReportsGenerator.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.IO; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.Gen.ComplianceReports; +using Microsoft.Gen.MetricsReports; +using Microsoft.Gen.Shared; +using Microsoft.Shared.DiagnosticIds; + +namespace Microsoft.Gen.MetadataExtractor; + +/// +/// Generates reports for compliance & metrics annotations. +/// +[Generator] +public sealed class MetadataReportsGenerator : ISourceGenerator +{ + private const string GenerateMetadataMSBuildProperty = "build_property.GenerateMetadataReport"; + private const string ReportOutputPathMSBuildProperty = "build_property.MetadataReportOutputPath"; + private const string RootNamespace = "build_property.rootnamespace"; + private const string FallbackFileName = "MetadataReport.json"; + private readonly string _fileName; + + /// + /// Initializes a new instance of the class. + /// + public MetadataReportsGenerator() + : this(FallbackFileName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The report file name. + public MetadataReportsGenerator(string reportFileName) + { + _fileName = reportFileName; + } + + /// + /// Initializes the generator. + /// + /// The generator initialization context. + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create); + } + + /// + /// Generates reports for compliance & metrics annotations. + /// + /// The generator execution context. + public void Execute(GeneratorExecutionContext context) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (context.SyntaxReceiver is not TypeDeclarationSyntaxReceiver || + ((TypeDeclarationSyntaxReceiver)context.SyntaxReceiver).TypeDeclarations.Count == 0 || + !GeneratorUtilities.ShouldGenerateReport(context, GenerateMetadataMSBuildProperty)) + { + return; + } + + if ((context.SyntaxReceiver is not TypeDeclarationSyntaxReceiver || ((TypeDeclarationSyntaxReceiver)context.SyntaxReceiver).TypeDeclarations.Count == 0)) + { + // nothing to do yet + return; + } + + var options = context.AnalyzerConfigOptions.GlobalOptions; + var path = GeneratorUtilities.TryRetrieveOptionsValue(options, ReportOutputPathMSBuildProperty, out var reportOutputPath) + ? reportOutputPath! + : GeneratorUtilities.GetDefaultReportOutputPath(options); + if (string.IsNullOrWhiteSpace(path)) + { + // Report diagnostic: + var diagnostic = new DiagnosticDescriptor( + DiagnosticIds.AuditReports.AUDREPGEN000, + "MetricsReports generator couldn't resolve output path for the report. It won't be generated.", + "Both and MSBuild properties are not set. The report won't be generated.", + nameof(DiagnosticIds.AuditReports), + DiagnosticSeverity.Info, + isEnabledByDefault: true, + helpLinkUri: string.Format(CultureInfo.InvariantCulture, DiagnosticIds.UrlFormat, DiagnosticIds.AuditReports.AUDREPGEN000)); + + context.ReportDiagnostic(Diagnostic.Create(diagnostic, location: null)); + return; + } + + (string metricReport, string complianceReport) metadataReport = (string.Empty, string.Empty); + metadataReport.metricReport = HandleMetricReportGeneration(context, (TypeDeclarationSyntaxReceiver)context.SyntaxReceiver); + metadataReport.complianceReport = HandleComplianceReportGeneration(context, (TypeDeclarationSyntaxReceiver)context.SyntaxReceiver); + + StringBuilder reportStringBuilder = new StringBuilder() + .Append("{ \"Name\": \"") + .Append(context.Compilation.AssemblyName!) + .Append("\", \"ComplianceReport\": ") + .Append((string.IsNullOrEmpty(metadataReport.complianceReport) ? "{}" : metadataReport.complianceReport)) + .Append(" ,") + .Append(" \"MetricReport\": ") + .Append((string.IsNullOrEmpty(metadataReport.metricReport) ? "[]" : metadataReport.metricReport) + " }"); + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + File.WriteAllText(Path.Combine(path, _fileName), reportStringBuilder.ToString(), Encoding.UTF8); +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + + } + + /// + /// used to generate the report for metrics annotations. + /// + /// The generator execution context. + /// The typeDeclaration syntax receiver. + /// string report as json or String.Empty. + private static string HandleMetricReportGeneration(GeneratorExecutionContext context, TypeDeclarationSyntaxReceiver receiver) + { + var meteringParser = new Metrics.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + var meteringClasses = meteringParser.GetMetricClasses(receiver.TypeDeclarations); + + if (meteringClasses.Count == 0) + { + return string.Empty; + } + + _ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(RootNamespace, out var rootNamespace); + var reportedMetrics = MetricsReportsHelpers.MapToCommonModel(meteringClasses, rootNamespace); + var emitter = new MetricDefinitionEmitter(); + var report = emitter.GenerateReport(reportedMetrics, context.CancellationToken); + return report; + } + + /// + /// used to generate the report for compliance annotations. + /// + /// The generator execution context. + /// The type declaration syntax receiver. + /// string report as json or String.Empty. + private static string HandleComplianceReportGeneration(GeneratorExecutionContext context, TypeDeclarationSyntaxReceiver receiver) + { + if (!SymbolLoader.TryLoad(context.Compilation, out var symbolHolder)) + { + return string.Empty; + } + + var parser = new Parser(context.Compilation, symbolHolder!, context.CancellationToken); + var classifiedTypes = parser.GetClassifiedTypes(receiver.TypeDeclarations); + if (classifiedTypes.Count == 0) + { + // nothing to do + return string.Empty; + } + + var emitter = new Emitter(); + string report = emitter.Emit(classifiedTypes, context.Compilation.AssemblyName!, false); + + return report; + } +} diff --git a/src/Generators/Microsoft.Gen.MetadataExtractor/Microsoft.Gen.MetadataExtractor.csproj b/src/Generators/Microsoft.Gen.MetadataExtractor/Microsoft.Gen.MetadataExtractor.csproj new file mode 100644 index 00000000000..a30f7e1eba4 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MetadataExtractor/Microsoft.Gen.MetadataExtractor.csproj @@ -0,0 +1,57 @@ + + + Microsoft.Gen.MetadataExtractor + Produces compliance and metrics reports based on data classification annotations in the code. + Fundamentals + + + + cs + true + true + + + + normal + 98 + 85 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsGenerator.cs b/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsGenerator.cs index bc5df1036d1..0f2e9937d2b 100644 --- a/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsGenerator.cs +++ b/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsGenerator.cs @@ -1,14 +1,10 @@ // 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 System.Globalization; using System.IO; -using System.Linq; using System.Text; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.Gen.Metrics.Model; using Microsoft.Gen.Shared; using Microsoft.Shared.DiagnosticIds; @@ -21,7 +17,6 @@ public class MetricsReportsGenerator : ISourceGenerator private const string RootNamespace = "build_property.rootnamespace"; private const string ReportOutputPath = "build_property.MetricsReportOutputPath"; private const string FileName = "MetricsReport.json"; - private readonly string _fileName; public MetricsReportsGenerator() @@ -42,7 +37,6 @@ public void Initialize(GeneratorInitializationContext context) public void Execute(GeneratorExecutionContext context) { context.CancellationToken.ThrowIfCancellationRequested(); - if (context.SyntaxReceiver is not ClassDeclarationSyntaxReceiver receiver || receiver.ClassDeclarations.Count == 0 || !GeneratorUtilities.ShouldGenerateReport(context, GenerateMetricDefinitionReport)) @@ -50,15 +44,6 @@ public void Execute(GeneratorExecutionContext context) return; } - var meteringParser = new Metrics.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); - - var meteringClasses = meteringParser.GetMetricClasses(receiver.ClassDeclarations); - - if (meteringClasses.Count == 0) - { - return; - } - var options = context.AnalyzerConfigOptions.GlobalOptions; var path = GeneratorUtilities.TryRetrieveOptionsValue(options, ReportOutputPath, out var reportOutputPath) @@ -76,16 +61,20 @@ public void Execute(GeneratorExecutionContext context) DiagnosticSeverity.Info, isEnabledByDefault: true, helpLinkUri: string.Format(CultureInfo.InvariantCulture, DiagnosticIds.UrlFormat, DiagnosticIds.AuditReports.AUDREPGEN000)); - context.ReportDiagnostic(Diagnostic.Create(diagnostic, location: null)); + return; + } + var meteringParser = new Metrics.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken); + var meteringClasses = meteringParser.GetMetricClasses(receiver.ClassDeclarations); + if (meteringClasses.Count == 0) + { return; } _ = options.TryGetValue(RootNamespace, out var rootNamespace); - + var reportedMetrics = MetricsReportsHelpers.MapToCommonModel(meteringClasses, rootNamespace); var emitter = new MetricDefinitionEmitter(); - var reportedMetrics = MapToCommonModel(meteringClasses, rootNamespace); var report = emitter.GenerateReport(reportedMetrics, context.CancellationToken); // File IO has been marked as banned for use in analyzers, and an alternate should be used instead @@ -95,23 +84,4 @@ public void Execute(GeneratorExecutionContext context) File.WriteAllText(Path.Combine(path, _fileName), report, Encoding.UTF8); #pragma warning restore RS1035 // Do not use APIs banned for analyzers } - - private static ReportedMetricClass[] MapToCommonModel(IReadOnlyList meteringClasses, string? rootNamespace) - { - var reportedMetrics = meteringClasses - .Select(meteringClass => new ReportedMetricClass( - Name: meteringClass.Name, - RootNamespace: rootNamespace ?? meteringClass.Namespace, - Constraints: meteringClass.Constraints, - Modifiers: meteringClass.Modifiers, - Methods: meteringClass.Methods.Select(meteringMethod => new ReportedMetricMethod( - MetricName: meteringMethod.MetricName ?? "(Missing Name)", - Summary: meteringMethod.XmlDefinition ?? "(Missing Summary)", - Kind: meteringMethod.InstrumentKind, - Dimensions: meteringMethod.TagKeys, - DimensionsDescriptions: meteringMethod.TagDescriptionDictionary)) - .ToArray())); - - return reportedMetrics.ToArray(); - } } diff --git a/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs b/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs new file mode 100644 index 00000000000..ee610c0c032 --- /dev/null +++ b/src/Generators/Microsoft.Gen.MetricsReports/MetricsReportsHelpers.cs @@ -0,0 +1,29 @@ +// 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 System.Linq; +using Microsoft.Gen.Metrics.Model; + +namespace Microsoft.Gen.MetricsReports; +internal static class MetricsReportsHelpers +{ + internal static ReportedMetricClass[] MapToCommonModel(IReadOnlyList meteringClasses, string? rootNamespace) + { + var reportedMetrics = meteringClasses + .Select(meteringClass => new ReportedMetricClass( + Name: meteringClass.Name, + RootNamespace: rootNamespace ?? meteringClass.Namespace, + Constraints: meteringClass.Constraints, + Modifiers: meteringClass.Modifiers, + Methods: meteringClass.Methods.Select(meteringMethod => new ReportedMetricMethod( + MetricName: meteringMethod.MetricName ?? "(Missing Name)", + Summary: meteringMethod.XmlDefinition ?? "(Missing Summary)", + Kind: meteringMethod.InstrumentKind, + Dimensions: meteringMethod.TagKeys, + DimensionsDescriptions: meteringMethod.TagDescriptionDictionary)) + .ToArray())); + + return reportedMetrics.ToArray(); + } +} diff --git a/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj b/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj index b7e8af3a8c8..0cfbf11dc1d 100644 --- a/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj +++ b/src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj @@ -14,13 +14,14 @@ n/a + - - + + - + diff --git a/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props b/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props index 978d52a35ea..b791bf81860 100644 --- a/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props +++ b/src/Packages/Microsoft.Extensions.AuditReports/buildTransitive/Microsoft.Extensions.AuditReports.props @@ -9,11 +9,18 @@ true + + false + true + + + + + - - + diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/Basic.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/Basic.json new file mode 100644 index 00000000000..2e9867dc658 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/Basic.json @@ -0,0 +1,118 @@ +{ "Name": "test.dll", "ComplianceReport": +{ + "Types": [ + { + "Name": "Test.Basic", + "Members": [ + { + "Name": "F0", + "Type": "int", + "File": "src-0.cs", + "Line": "18", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C2", + "Notes": "Note 1" + } + ] + }, + { + "Name": "F1", + "Type": "int", + "File": "src-0.cs", + "Line": "21", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C2" + } + ] + }, + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C3", + "Notes": "Note 2" + }, + { + "Name": "C4" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "27", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C3" + } + ] + } + ], + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "30", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "30" + } + ] + }, + { + "Name": "LogWorld", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "33", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "33" + } + ] + } + ] + } + ] +} , "MetricReport": [] } \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/Inheritance.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/Inheritance.json new file mode 100644 index 00000000000..084f52598d5 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/Inheritance.json @@ -0,0 +1,62 @@ +{ "Name": "test.dll", "ComplianceReport": +{ + "Types": [ + { + "Name": "Test.Base", + "Members": [ + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C1" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C2" + } + ] + } + ] + }, + { + "Name": "Test.Inherited", + "Members": [ + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C1" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C2" + }, + { + "Name": "C3" + } + ] + } + ] + } + ] +} , "MetricReport": [] } \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/LogMethod.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/LogMethod.json new file mode 100644 index 00000000000..8f14aa9c92f --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/LogMethod.json @@ -0,0 +1,33 @@ +{ "Name": "test.dll", "ComplianceReport": +{ + "Types": [ + { + "Name": "Test.LogMethod", + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "11", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "11" + } + ] + } + ] + } + ] +} , "MetricReport": [] } \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterAttributedWithXmlDescriptions.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterAttributedWithXmlDescriptions.json new file mode 100644 index 00000000000..c89592c2bf7 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterAttributedWithXmlDescriptions.json @@ -0,0 +1,31 @@ +{ "Name": "test.dll", "ComplianceReport": {} , "MetricReport": [ + { + "TestClasses": + [ + { + "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.MetadataExtractor/GoldenReports/MeterAttributedWithXmlDescriptions_RecordProperty.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterAttributedWithXmlDescriptions_RecordProperty.json new file mode 100644 index 00000000000..270a4b1d979 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterAttributedWithXmlDescriptions_RecordProperty.json @@ -0,0 +1,230 @@ +{ "Name": "test.dll", "ComplianceReport": +{ + "Types": [ + { + "Name": "Test.DerivedRecordProperty", + "Members": [ + { + "Name": "EqualityContract", + "Type": "System.Type", + "File": "src-0.cs", + "Line": "18", + "Classifications": [ + { + "Name": "C1" + } + ] + }, + { + "Name": "F3", + "Type": "int", + "File": "src-0.cs", + "Line": "21", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 1" + } + ] + }, + { + "Name": "F4", + "Type": "int", + "File": "src-0.cs", + "Line": "24", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "27", + "Classifications": [ + { + "Name": "C1" + }, + { + "Name": "C2", + "Notes": "Note 2" + }, + { + "Name": "C3", + "Notes": "Note 3" + }, + { + "Name": "C4" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "30", + "Classifications": [ + { + "Name": "C3" + } + ] + } + ] + }, + { + "Name": "Test.RecordProperty", + "Members": [ + { + "Name": "F0", + "Type": "string", + "File": "src-0.cs", + "Line": "18", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "F2", + "Type": "int", + "File": "src-0.cs", + "Line": "18", + "Classifications": [ + { + "Name": "C3" + } + ] + }, + { + "Name": "F3", + "Type": "int", + "File": "src-0.cs", + "Line": "21", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 1" + } + ] + }, + { + "Name": "F4", + "Type": "int", + "File": "src-0.cs", + "Line": "24", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "P0", + "Type": "int", + "File": "src-0.cs", + "Line": "15", + "Classifications": [ + { + "Name": "C3", + "Notes": "Note 3" + }, + { + "Name": "C4" + } + ] + }, + { + "Name": "P1", + "Type": "int", + "File": "src-0.cs", + "Line": "30", + "Classifications": [ + { + "Name": "C3" + } + ] + } + ], + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "33", + "Classifications": [ + { + "Name": "C3", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "33" + } + ] + }, + { + "Name": "LogWorld", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "36", + "Classifications": [ + { + "Name": "C2" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "36" + } + ] + } + ] + } + ] +} , "MetricReport": [ + { + "TestClasses": + [ + { + "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.MetadataExtractor/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions.json new file mode 100644 index 00000000000..3c58e7c0670 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions.json @@ -0,0 +1,39 @@ +{ "Name": "test.dll", "ComplianceReport": {} , "MetricReport": [ + { + "TestClasses": + [ + { + "MetricName": "DescribedDimensionCounter", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "Dimension1": "Dimension1 description.", + "Dim1": "" + } + }, + { + "MetricName": "DescribedDimensionHistogram", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Histogram", + "Dimensions": { + "Dimension2": "Dimension2 description.", + "DimensionDefinedInMetricClass": "DimensionDefinedInMetricClass 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.MetadataExtractor/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions_LogMethod.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions_LogMethod.json new file mode 100644 index 00000000000..021cf13caa6 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/MeterDimensionsAttributedWithXmlDescriptions_LogMethod.json @@ -0,0 +1,71 @@ +{ "Name": "test.dll", "ComplianceReport": +{ + "Types": [ + { + "Name": "Test.LogMethod", + "Logging Methods": [ + { + "Name": "LogHello", + "Parameters": [ + { + "Name": "user", + "Type": "string", + "File": "src-0.cs", + "Line": "14", + "Classifications": [ + { + "Name": "C2", + "Notes": "Note 3" + } + ] + }, + { + "Name": "port", + "Type": "int", + "File": "src-0.cs", + "Line": "14" + } + ] + } + ] + } + ] +} , "MetricReport": [ + { + "TestClasses": + [ + { + "MetricName": "DescribedDimensionCounter", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "Dimension1": "Dimension1 description.", + "Dim1": "" + } + }, + { + "MetricName": "DescribedDimensionHistogram", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Histogram", + "Dimensions": { + "Dimension2": "Dimension2 description.", + "DimensionDefinedInMetricClass": "DimensionDefinedInMetricClass description." + } + }, + { + "MetricName": "MyStrongTypeMetricWithDescription", + "MetricDescription": "(Missing Summary)", + "InstrumentName": "Counter", + "Dimensions": { + "AnotherDimension": "Gets or sets anotherDimension.", + "MetricEnum": "Gets or sets MetricEnum.", + "MetricEnum2": "Gets or sets MetricEnum2.", + "Dim2": "Gets or sets Dim2.", + "SomeDim": "Gets or sets SomeDim.", + "Dim4Struct": "Gets or sets Dim4Struct.", + "Dim5Struct": "Gets or sets Dim5Struct." + } + } + ] + } +] } \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/RecordProperty.json b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/RecordProperty.json new file mode 100644 index 00000000000..7e78ce5d2ad --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/GoldenReports/RecordProperty.json @@ -0,0 +1,200 @@ +{ "Name": "test.dll", "ComplianceReport": +{ + "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" + } + ] + } + ] + } + ] +} , "MetricReport": [] } \ No newline at end of file diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/Basic.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/Basic.cs new file mode 100644 index 00000000000..a2dce688280 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/Basic.cs @@ -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 Microsoft.Extensions.Logging; + +namespace Test; + +interface IFoo +{ + [C4] + public int P0 { get; } +} + +[C1] +public class Basic : IFoo +{ + [C2(Notes = "Note 1")] + public int F0; + + [C2(Notes = null!)] + public int F1; + + [C3(Notes = "Note 2")] + public int P0 { get; } + + [C3] + public int P1 { get; } + + [LoggerMessage("Hello {user}")] + public void LogHello([C2(Notes = "Note 3")] string user, int port); + + [LoggerMessage("World {user}")] + public void LogWorld([C2] string user, int port); +} diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/Inheritance.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/Inheritance.cs new file mode 100644 index 00000000000..8c06e7682a1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/Inheritance.cs @@ -0,0 +1,21 @@ +// 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 Test; + +public class Base +{ + [C1] + public int P0 { get; } + + [C2] + public virtual int P1 { get; } +} + +public class Inherited : Base +{ + [C3] + public override int P1 { get; } +} diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/LogMethod.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/LogMethod.cs new file mode 100644 index 00000000000..543f086b431 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/LogMethod.cs @@ -0,0 +1,12 @@ +// 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 Test; + +public class LogMethod +{ + [LoggerMessage("Hello {user}")] + public void LogHello([C2(Notes = "Note 3")] string user, int port); +} diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterAttributedWithXmlDescriptions.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterAttributedWithXmlDescriptions.cs new file mode 100644 index 00000000000..0ab35cdcea1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterAttributedWithXmlDescriptions.cs @@ -0,0 +1,54 @@ +// 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.Diagnostics.Metrics; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "For testing emitter for classes with description for metrics.")] + 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.MetadataExtractor/TestClasses/MeterAttributedWithXmlDescriptions_RecordProperty.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterAttributedWithXmlDescriptions_RecordProperty.cs new file mode 100644 index 00000000000..424309b82b0 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterAttributedWithXmlDescriptions_RecordProperty.cs @@ -0,0 +1,92 @@ +// 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.Diagnostics.Metrics; +using Microsoft.Extensions.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; }; + + [LoggerMessage("Hello {user}")] + public void LogHello([C3(Notes = "Note 3")] string user, int port); + + [LoggerMessage("World {user}")] + public void LogWorld([C2] string user, int port); +} + +[C1] +public record DerivedRecordProperty : RecordProperty +{ + [C2(Notes = "Note 2")] + public override int P0 { get; }; +} +} +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "For testing emitter for classes with description for metrics.")] + 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.MetadataExtractor/TestClasses/MeterDimensionsAttributedWithXmlDescriptions.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterDimensionsAttributedWithXmlDescriptions.cs new file mode 100644 index 00000000000..3777e60d353 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterDimensionsAttributedWithXmlDescriptions.cs @@ -0,0 +1,118 @@ +// 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.Diagnostics.Metrics; + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes with description for metrics.")] + Justification = "Metrics generator tests")] + internal static partial class MeterDimensionsAttributedWithXmlDescriptions + { + public const string Dim1 = "Dim1"; + + [Counter(DescriptedDimensions.Dimension1, Dim1)] + public static partial DescribedDimensionCounter CreatePublicCounter(Meter meter); + + /// + /// DimensionDefinedInMetricClass description. + /// + public const string DimensionDefinedInMetricClass = "DimensionDefinedInMetricClass"; + + [Histogram(DescriptedDimensions.Dimension2, DimensionDefinedInMetricClass)] + public static partial DescribedDimensionHistogram CreatePublicHistogram(Meter meter); + + [Counter(typeof(DimensionForStrongTypes), Name = "MyStrongTypeMetricWithDescription")] + public static partial StrongTypeCounterWithDescriptedDimension CreateStrongTypeCounterWithDescribedDimensions(Meter meter); + } + +#pragma warning disable SA1402 // File may only contain a single type + + /// + /// DescriptedDimensions class description. + /// + internal static class DescriptedDimensions + { + /// + /// 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. + /// + [TagName("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. + /// + [TagName("dim2FromAttribute")] + public string? SomeDim; + } + + public struct DimensionForStrongTypesDimensionsStruct + { + /// + /// Gets or sets Dim4Struct. + /// + public string Dim4Struct { get; set; } + + /// + /// Gets or sets Dim5Struct. + /// + [TagName("Dim5FromAttribute")] + public string Dim5Struct { get; set; } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterDimensionsAttributedWithXmlDescriptions_LogMethod.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterDimensionsAttributedWithXmlDescriptions_LogMethod.cs new file mode 100644 index 00000000000..faf6044bd9a --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/MeterDimensionsAttributedWithXmlDescriptions_LogMethod.cs @@ -0,0 +1,129 @@ +// 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.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; + +namespace Test{ + +public class LogMethod +{ + [LoggerMessage("Hello {user}")] + public void LogHello([C2(Notes = "Note 3")] string user, int port); +} +} + + +namespace TestClasses +{ + [SuppressMessage("Usage", "CA1801:Review unused parameters", + Justification = "For testing emitter for classes with description for metrics.")] + Justification = "Metrics generator tests")] + internal static partial class MeterDimensionsAttributedWithXmlDescriptions + { + public const string Dim1 = "Dim1"; + + [Counter(DescriptedDimensions.Dimension1, Dim1)] + public static partial DescribedDimensionCounter CreatePublicCounter(Meter meter); + + /// + /// DimensionDefinedInMetricClass description. + /// + public const string DimensionDefinedInMetricClass = "DimensionDefinedInMetricClass"; + + [Histogram(DescriptedDimensions.Dimension2, DimensionDefinedInMetricClass)] + public static partial DescribedDimensionHistogram CreatePublicHistogram(Meter meter); + + [Counter(typeof(DimensionForStrongTypes), Name = "MyStrongTypeMetricWithDescription")] + public static partial StrongTypeCounterWithDescriptedDimension CreateStrongTypeCounterWithDescribedDimensions(Meter meter); + } + +#pragma warning disable SA1402 // File may only contain a single type + + /// + /// DescriptedDimensions class description. + /// + internal static class DescriptedDimensions + { + /// + /// 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. + /// + [TagName("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. + /// + [TagName("dim2FromAttribute")] + public string? SomeDim; + } + + public struct DimensionForStrongTypesDimensionsStruct + { + /// + /// Gets or sets Dim4Struct. + /// + public string Dim4Struct { get; set; } + + /// + /// Gets or sets Dim5Struct. + /// + [TagName("Dim5FromAttribute")] + public string Dim5Struct { get; set; } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/RecordProperty.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/TestClasses/RecordProperty.cs new file mode 100644 index 00000000000..b38940ed8bf --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/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.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; }; + + [LoggerMessage("Hello {user}")] + public void LogHello([C3(Notes = "Note 3")] string user, int port); + + [LoggerMessage("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.MetadataExtractor/Unit/GeneratorTests.cs b/test/Generators/Microsoft.Gen.MetadataExtractor/Unit/GeneratorTests.cs new file mode 100644 index 00000000000..5e7c749d426 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/Unit/GeneratorTests.cs @@ -0,0 +1,259 @@ +// 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.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; +using Microsoft.Gen.Shared; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Gen.MetadataExtractor.Unit.Tests; + +/// +/// Tests for the . +/// +/// The test output helper. +public class GeneratorTests(ITestOutputHelper output) +{ + private const string ReportFilename = "MetadataReport.json"; + private const string TestTaxonomy = @" + using Microsoft.Extensions.Compliance.Classification; + + public sealed class C1Attribute : DataClassificationAttribute + { + public C1Attribute(new DataClassification(""TAX"", 1)) { } + } + + public sealed class C2Attribute : DataClassificationAttribute + { + public C2Attribute(new DataClassification(""TAX"", 2)) { } + } + + public sealed class C3Attribute : DataClassificationAttribute + { + public C3Attribute(new DataClassification(""TAX"", 4)) { } + } + + public sealed class C4Attribute : DataClassificationAttribute + { + public C4Attribute(new DataClassification(""TAX"", 8)) { } + } + "; + + /// + /// Generator should not do anything if general execution context does not have class declaration. + /// + [Fact] + public void GeneratorShouldNotDoAnythingIfGeneralExecutionContextDoesNotHaveClassDeclarationSyntaxReceiver() + { + GeneratorExecutionContext defaultGeneralExecutionContext = default; + new MetadataReportsGenerator().Execute(defaultGeneralExecutionContext); + + Assert.Null(defaultGeneralExecutionContext.SyntaxReceiver); + } + + /// + /// Tests Generations for both compliance & metric or both or none. + /// + /// The Use Explicit Report Path. + [Theory] + [CombinatorialData] + public async Task TestAll(bool useExplicitReportPath) + { + Dictionary? options = useExplicitReportPath + ? new() { ["build_property.MetadataReportOutputPath"] = Directory.GetCurrentDirectory() } + : null; + + foreach (var inputFile in Directory.GetFiles("TestClasses")) + { + var stem = Path.GetFileNameWithoutExtension(inputFile); + var goldenFileName = Path.ChangeExtension(stem, ".json"); + var goldenReportPath = Path.Combine("GoldenReports", goldenFileName); + + var generatedReportPath = Path.Combine(Directory.GetCurrentDirectory(), ReportFilename); + + if (File.Exists(goldenReportPath)) + { + var d = await RunGenerator(await File.ReadAllTextAsync(inputFile), options); + Assert.Empty(d); + + var golden = await File.ReadAllTextAsync(goldenReportPath); + var generated = await File.ReadAllTextAsync(generatedReportPath); + + if (golden != generated) + { + output.WriteLine($"MISMATCH: golden report {goldenReportPath}, generated {generatedReportPath}"); + output.WriteLine("----"); + output.WriteLine("golden:"); + output.WriteLine(golden); + output.WriteLine("----"); + output.WriteLine("generated:"); + output.WriteLine(generated); + output.WriteLine("----"); + } + + File.Delete(generatedReportPath); + + Assert.Equal(golden, generated); + } + else + { + // generate the golden file if it doesn't already exist + output.WriteLine($"Generating golden report: {goldenReportPath}"); + _ = await RunGenerator(await File.ReadAllTextAsync(inputFile), options, reportFileName: goldenFileName); + } + } + } + + /// + /// Generator should not do anything if there are no class declarations. + /// + [Fact] + public async Task ShouldNot_Generate_WhenDisabledViaConfig() + { + var inputFile = Directory.GetFiles("TestClasses").First(); + var options = new Dictionary + { + ["build_property.GenerateMetadataReport"] = bool.FalseString, + ["build_property.MetadataReportOutputPath"] = Path.GetTempPath(), + ["build_property.rootnamespace"] = "TestClasses" + }; + + var d = await RunGenerator(await File.ReadAllTextAsync(inputFile), options); + Assert.Empty(d); + Assert.False(File.Exists(Path.Combine(Path.GetTempPath(), ReportFilename))); + } + + /// + /// Generator should emit warning when path is not provided. + /// + /// If the report path is provided. + [Theory] + [CombinatorialData] + public async Task Should_EmitWarning_WhenPathUnavailable(bool isReportPathProvided) + { + var inputFile = Directory.GetFiles("TestClasses").First(); + var options = new Dictionary + { + ["build_property.outputpath"] = string.Empty + }; + + if (isReportPathProvided) + { + options.Add("build_property.MetadataReportOutputPath", string.Empty); + } + + var diags = await RunGenerator(await File.ReadAllTextAsync(inputFile), options); + var diag = Assert.Single(diags); + Assert.Equal("AUDREPGEN000", diag.Id); + Assert.Equal(DiagnosticSeverity.Info, diag.Severity); + } + + /// + /// Generator should emit warning when path is not provided. + /// + [Fact] + public async Task Should_UseProjectDir_WhenOutputPathIsRelative() + { + var projectDir = Path.GetTempPath(); + var outputPath = Guid.NewGuid().ToString(); + var fullReportPath = Path.Combine(projectDir, outputPath); + Directory.CreateDirectory(fullReportPath); + + try + { + var inputFile = Directory.GetFiles("TestClasses").First(); + var options = new Dictionary + { + ["build_property.projectdir"] = projectDir, + ["build_property.MetadataReportOutputPath"] = fullReportPath + }; + + var diags = await RunGenerator(await File.ReadAllTextAsync(inputFile), options); + Assert.Empty(diags); + Assert.True(File.Exists(Path.Combine(fullReportPath, ReportFilename))); + } + finally + { + Directory.Delete(fullReportPath, recursive: true); + } + } + + /// + /// Runs the generator on the given code. + /// + /// The coded that the generation will be based-on. + /// The analyzer options. + /// The cancellation Token. + /// The report file name. + private static async Task> RunGenerator( + string code, + Dictionary? analyzerOptions = null, + CancellationToken cancellationToken = default, + string? reportFileName = null) + { + Assembly[] refs = + [ + Assembly.GetAssembly(typeof(Meter))!, + Assembly.GetAssembly(typeof(CounterAttribute))!, + Assembly.GetAssembly(typeof(HistogramAttribute))!, + Assembly.GetAssembly(typeof(GaugeAttribute))!, + Assembly.GetAssembly(typeof(ILogger))!, + Assembly.GetAssembly(typeof(LoggerMessageAttribute))!, + Assembly.GetAssembly(typeof(Extensions.Compliance.Classification.DataClassification))!, + ]; + + var generator = reportFileName is null + ? new MetadataReportsGenerator() + : new MetadataReportsGenerator(reportFileName); + + var (d, _) = await RoslynTestUtils.RunGenerator( + generator, + refs, + new[] { code, TestTaxonomy }, + new OptionsProvider(analyzerOptions), + includeBaseReferences: true, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return d; + } + + /// + /// Options for the generator. + /// + private sealed class Options : AnalyzerConfigOptions + { + private readonly Dictionary _options; + + public Options(Dictionary? analyzerOptions) + { + _options = analyzerOptions ?? []; + _options.TryAdd("build_property.GenerateMetadataReport", bool.TrueString); + _options.TryAdd("build_property.outputpath", Directory.GetCurrentDirectory()); + } + + public override bool TryGetValue(string key, out string value) + => _options.TryGetValue(key, out value!); + } + + /// + /// Options provider for the generator. + /// + /// The analyzer options. + private sealed class OptionsProvider(Dictionary? analyzerOptions) : AnalyzerConfigOptionsProvider + { + public override AnalyzerConfigOptions GlobalOptions => new Options(analyzerOptions); + 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.MetadataExtractor/Unit/Microsoft.Gen.MetadataExtractor.Unit.Tests.csproj b/test/Generators/Microsoft.Gen.MetadataExtractor/Unit/Microsoft.Gen.MetadataExtractor.Unit.Tests.csproj new file mode 100644 index 00000000000..34bfc75d1f1 --- /dev/null +++ b/test/Generators/Microsoft.Gen.MetadataExtractor/Unit/Microsoft.Gen.MetadataExtractor.Unit.Tests.csproj @@ -0,0 +1,33 @@ + + + Microsoft.Gen.MetadataExtractor.Test + Unit tests for Microsoft.Gen.MetadataExtractor. + + + + true + + + + + + + TestClasses\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + GoldenReports\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + + + + + + + + + +