Skip to content

Commit fc84cf9

Browse files
authored
Metadata reports generator (Issue #3999) (#5531)
Metadata reports generator (Issue #3999) (#5531)
1 parent 934f767 commit fc84cf9

26 files changed

+1940
-46
lines changed

src/Generators/Microsoft.Gen.ComplianceReports/Emitter.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ public Emitter()
2121
}
2222

2323
[SuppressMessage("Performance", "LA0002:Use 'Microsoft.Extensions.Text.NumericExtensions.ToInvariantString' for improved performance", Justification = "Can't use that in a generator")]
24-
public string Emit(IReadOnlyCollection<ClassifiedType> classifiedTypes, string assemblyName)
24+
public string Emit(IReadOnlyCollection<ClassifiedType> classifiedTypes, string assemblyName, bool includeName = true) // show or hide assemblyName in the report,defaulted to true.
2525
{
2626
OutObject(() =>
2727
{
28-
OutNameValue("Name", assemblyName);
28+
// 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
29+
if (includeName)
30+
{
31+
OutNameValue("Name", assemblyName);
32+
}
2933

3034
OutArray("Types", () =>
3135
{
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
using System.IO;
6+
using System.Text;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.Gen.ComplianceReports;
9+
using Microsoft.Gen.MetricsReports;
10+
using Microsoft.Gen.Shared;
11+
using Microsoft.Shared.DiagnosticIds;
12+
13+
namespace Microsoft.Gen.MetadataExtractor;
14+
15+
/// <summary>
16+
/// Generates reports for compliance & metrics annotations.
17+
/// </summary>
18+
[Generator]
19+
public sealed class MetadataReportsGenerator : ISourceGenerator
20+
{
21+
private const string GenerateMetadataMSBuildProperty = "build_property.GenerateMetadataReport";
22+
private const string ReportOutputPathMSBuildProperty = "build_property.MetadataReportOutputPath";
23+
private const string RootNamespace = "build_property.rootnamespace";
24+
private const string FallbackFileName = "MetadataReport.json";
25+
private readonly string _fileName;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="MetadataReportsGenerator"/> class.
29+
/// </summary>
30+
public MetadataReportsGenerator()
31+
: this(FallbackFileName)
32+
{
33+
}
34+
35+
/// <summary>
36+
/// Initializes a new instance of the <see cref="MetadataReportsGenerator"/> class.
37+
/// </summary>
38+
/// <param name="reportFileName">The report file name.</param>
39+
public MetadataReportsGenerator(string reportFileName)
40+
{
41+
_fileName = reportFileName;
42+
}
43+
44+
/// <summary>
45+
/// Initializes the generator.
46+
/// </summary>
47+
/// <param name="context">The generator initialization context.</param>
48+
public void Initialize(GeneratorInitializationContext context)
49+
{
50+
context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create);
51+
}
52+
53+
/// <summary>
54+
/// Generates reports for compliance & metrics annotations.
55+
/// </summary>
56+
/// <param name="context">The generator execution context.</param>
57+
public void Execute(GeneratorExecutionContext context)
58+
{
59+
context.CancellationToken.ThrowIfCancellationRequested();
60+
61+
if (context.SyntaxReceiver is not TypeDeclarationSyntaxReceiver ||
62+
((TypeDeclarationSyntaxReceiver)context.SyntaxReceiver).TypeDeclarations.Count == 0 ||
63+
!GeneratorUtilities.ShouldGenerateReport(context, GenerateMetadataMSBuildProperty))
64+
{
65+
return;
66+
}
67+
68+
if ((context.SyntaxReceiver is not TypeDeclarationSyntaxReceiver || ((TypeDeclarationSyntaxReceiver)context.SyntaxReceiver).TypeDeclarations.Count == 0))
69+
{
70+
// nothing to do yet
71+
return;
72+
}
73+
74+
var options = context.AnalyzerConfigOptions.GlobalOptions;
75+
var path = GeneratorUtilities.TryRetrieveOptionsValue(options, ReportOutputPathMSBuildProperty, out var reportOutputPath)
76+
? reportOutputPath!
77+
: GeneratorUtilities.GetDefaultReportOutputPath(options);
78+
if (string.IsNullOrWhiteSpace(path))
79+
{
80+
// Report diagnostic:
81+
var diagnostic = new DiagnosticDescriptor(
82+
DiagnosticIds.AuditReports.AUDREPGEN000,
83+
"MetricsReports generator couldn't resolve output path for the report. It won't be generated.",
84+
"Both <MetadataReportOutputPath> and <OutputPath> MSBuild properties are not set. The report won't be generated.",
85+
nameof(DiagnosticIds.AuditReports),
86+
DiagnosticSeverity.Info,
87+
isEnabledByDefault: true,
88+
helpLinkUri: string.Format(CultureInfo.InvariantCulture, DiagnosticIds.UrlFormat, DiagnosticIds.AuditReports.AUDREPGEN000));
89+
90+
context.ReportDiagnostic(Diagnostic.Create(diagnostic, location: null));
91+
return;
92+
}
93+
94+
(string metricReport, string complianceReport) metadataReport = (string.Empty, string.Empty);
95+
metadataReport.metricReport = HandleMetricReportGeneration(context, (TypeDeclarationSyntaxReceiver)context.SyntaxReceiver);
96+
metadataReport.complianceReport = HandleComplianceReportGeneration(context, (TypeDeclarationSyntaxReceiver)context.SyntaxReceiver);
97+
98+
StringBuilder reportStringBuilder = new StringBuilder()
99+
.Append("{ \"Name\": \"")
100+
.Append(context.Compilation.AssemblyName!)
101+
.Append("\", \"ComplianceReport\": ")
102+
.Append((string.IsNullOrEmpty(metadataReport.complianceReport) ? "{}" : metadataReport.complianceReport))
103+
.Append(" ,")
104+
.Append(" \"MetricReport\": ")
105+
.Append((string.IsNullOrEmpty(metadataReport.metricReport) ? "[]" : metadataReport.metricReport) + " }");
106+
107+
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
108+
File.WriteAllText(Path.Combine(path, _fileName), reportStringBuilder.ToString(), Encoding.UTF8);
109+
#pragma warning restore RS1035 // Do not use APIs banned for analyzers
110+
111+
}
112+
113+
/// <summary>
114+
/// used to generate the report for metrics annotations.
115+
/// </summary>
116+
/// <param name="context">The generator execution context.</param>
117+
/// <param name="receiver">The typeDeclaration syntax receiver.</param>
118+
/// <returns>string report as json or String.Empty.</returns>
119+
private static string HandleMetricReportGeneration(GeneratorExecutionContext context, TypeDeclarationSyntaxReceiver receiver)
120+
{
121+
var meteringParser = new Metrics.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken);
122+
var meteringClasses = meteringParser.GetMetricClasses(receiver.TypeDeclarations);
123+
124+
if (meteringClasses.Count == 0)
125+
{
126+
return string.Empty;
127+
}
128+
129+
_ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(RootNamespace, out var rootNamespace);
130+
var reportedMetrics = MetricsReportsHelpers.MapToCommonModel(meteringClasses, rootNamespace);
131+
var emitter = new MetricDefinitionEmitter();
132+
var report = emitter.GenerateReport(reportedMetrics, context.CancellationToken);
133+
return report;
134+
}
135+
136+
/// <summary>
137+
/// used to generate the report for compliance annotations.
138+
/// </summary>
139+
/// <param name="context">The generator execution context.</param>
140+
/// <param name="receiver">The type declaration syntax receiver.</param>
141+
/// <returns>string report as json or String.Empty.</returns>
142+
private static string HandleComplianceReportGeneration(GeneratorExecutionContext context, TypeDeclarationSyntaxReceiver receiver)
143+
{
144+
if (!SymbolLoader.TryLoad(context.Compilation, out var symbolHolder))
145+
{
146+
return string.Empty;
147+
}
148+
149+
var parser = new Parser(context.Compilation, symbolHolder!, context.CancellationToken);
150+
var classifiedTypes = parser.GetClassifiedTypes(receiver.TypeDeclarations);
151+
if (classifiedTypes.Count == 0)
152+
{
153+
// nothing to do
154+
return string.Empty;
155+
}
156+
157+
var emitter = new Emitter();
158+
string report = emitter.Emit(classifiedTypes, context.Compilation.AssemblyName!, false);
159+
160+
return report;
161+
}
162+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<RootNamespace>Microsoft.Gen.MetadataExtractor</RootNamespace>
4+
<Description>Produces compliance and metrics reports based on data classification annotations in the code.</Description>
5+
<Workstream>Fundamentals</Workstream>
6+
</PropertyGroup>
7+
8+
<PropertyGroup>
9+
<AnalyzerLanguage>cs</AnalyzerLanguage>
10+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
11+
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
12+
</PropertyGroup>
13+
14+
<PropertyGroup>
15+
<Stage>dev</Stage>
16+
<MinCodeCoverage>98</MinCodeCoverage>
17+
<MinMutationScore>85</MinMutationScore>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<Compile Include="..\Shared\TypeDeclarationSyntaxReceiver.cs" LinkBase="Shared" />
22+
<Compile Include="..\Shared\GeneratorUtilities.cs" LinkBase="Shared" />
23+
<Compile Include="..\Shared\ClassDeclarationSyntaxReceiver.cs" LinkBase="Shared" />
24+
<Compile Include="..\Shared\EmitterBase.cs" LinkBase="Shared" />
25+
<Compile Include="..\Shared\ParserUtilities.cs" LinkBase="Shared" />
26+
<Compile Include="..\Shared\DiagDescriptorsBase.cs" LinkBase="Shared" />
27+
<Compile Include="..\Shared\StringBuilderPool.cs" LinkBase="Shared" />
28+
<Compile Include="..\Microsoft.Gen.ComplianceReports\Model\*.cs" LinkBase="Microsoft.Gen.ComplianceReports" />
29+
<Compile Include="..\Microsoft.Gen.ComplianceReports\*.cs" LinkBase="Microsoft.Gen.ComplianceReports" />
30+
<Compile Include="..\Microsoft.Gen.Metrics\Exceptions\*.cs" LinkBase="Microsoft.Gen.Metrics" />
31+
<Compile Include="..\Microsoft.Gen.MetricsReports\*.cs" LinkBase="Microsoft.Gen.MetricsReports" />
32+
<Compile Include="..\Microsoft.Gen.Metrics\Model\*.cs" LinkBase="Microsoft.Gen.Metrics" />
33+
<Compile Include="..\Microsoft.Gen.Metrics\*.cs" LinkBase="Microsoft.Gen.Metrics" />
34+
<Compile Include="..\Microsoft.Gen.Shared\*.cs" LinkBase="Microsoft.Gen.Metrics" />
35+
<Compile Include="..\Microsoft.Gen.Shared\*.cs" LinkBase="Microsoft.Gen.Metrics" />
36+
37+
38+
39+
</ItemGroup>
40+
41+
<ItemGroup>
42+
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.ComplianceReports\Microsoft.Gen.ComplianceReports.csproj" />
43+
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.MetricsReports\Microsoft.Gen.MetricsReports.csproj" />
44+
</ItemGroup>
45+
46+
<ItemGroup>
47+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
48+
<PackageReference Include="Microsoft.CodeAnalysis" />
49+
</ItemGroup>
50+
51+
<ItemGroup>
52+
<ProjectReference Include="..\Microsoft.Gen.Metrics\Microsoft.Gen.Metrics.csproj" />
53+
</ItemGroup>
54+
<ItemGroup>
55+
<InternalsVisibleToTest Include="Microsoft.Gen.MetadataExtractor.Unit.Tests" />
56+
</ItemGroup>
57+
</Project>
Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Collections.Generic;
54
using System.Globalization;
65
using System.IO;
7-
using System.Linq;
86
using System.Text;
97
using Microsoft.CodeAnalysis;
10-
using Microsoft.CodeAnalysis.Diagnostics;
11-
using Microsoft.Gen.Metrics.Model;
128
using Microsoft.Gen.Shared;
139
using Microsoft.Shared.DiagnosticIds;
1410

@@ -21,7 +17,6 @@ public class MetricsReportsGenerator : ISourceGenerator
2117
private const string RootNamespace = "build_property.rootnamespace";
2218
private const string ReportOutputPath = "build_property.MetricsReportOutputPath";
2319
private const string FileName = "MetricsReport.json";
24-
2520
private readonly string _fileName;
2621

2722
public MetricsReportsGenerator()
@@ -42,23 +37,13 @@ public void Initialize(GeneratorInitializationContext context)
4237
public void Execute(GeneratorExecutionContext context)
4338
{
4439
context.CancellationToken.ThrowIfCancellationRequested();
45-
4640
if (context.SyntaxReceiver is not ClassDeclarationSyntaxReceiver receiver ||
4741
receiver.ClassDeclarations.Count == 0 ||
4842
!GeneratorUtilities.ShouldGenerateReport(context, GenerateMetricDefinitionReport))
4943
{
5044
return;
5145
}
5246

53-
var meteringParser = new Metrics.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken);
54-
55-
var meteringClasses = meteringParser.GetMetricClasses(receiver.ClassDeclarations);
56-
57-
if (meteringClasses.Count == 0)
58-
{
59-
return;
60-
}
61-
6247
var options = context.AnalyzerConfigOptions.GlobalOptions;
6348

6449
var path = GeneratorUtilities.TryRetrieveOptionsValue(options, ReportOutputPath, out var reportOutputPath)
@@ -76,16 +61,20 @@ public void Execute(GeneratorExecutionContext context)
7661
DiagnosticSeverity.Info,
7762
isEnabledByDefault: true,
7863
helpLinkUri: string.Format(CultureInfo.InvariantCulture, DiagnosticIds.UrlFormat, DiagnosticIds.AuditReports.AUDREPGEN000));
79-
8064
context.ReportDiagnostic(Diagnostic.Create(diagnostic, location: null));
65+
return;
66+
}
8167

68+
var meteringParser = new Metrics.Parser(context.Compilation, context.ReportDiagnostic, context.CancellationToken);
69+
var meteringClasses = meteringParser.GetMetricClasses(receiver.ClassDeclarations);
70+
if (meteringClasses.Count == 0)
71+
{
8272
return;
8373
}
8474

8575
_ = options.TryGetValue(RootNamespace, out var rootNamespace);
86-
76+
var reportedMetrics = MetricsReportsHelpers.MapToCommonModel(meteringClasses, rootNamespace);
8777
var emitter = new MetricDefinitionEmitter();
88-
var reportedMetrics = MapToCommonModel(meteringClasses, rootNamespace);
8978
var report = emitter.GenerateReport(reportedMetrics, context.CancellationToken);
9079

9180
// 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)
9584
File.WriteAllText(Path.Combine(path, _fileName), report, Encoding.UTF8);
9685
#pragma warning restore RS1035 // Do not use APIs banned for analyzers
9786
}
98-
99-
private static ReportedMetricClass[] MapToCommonModel(IReadOnlyList<MetricType> meteringClasses, string? rootNamespace)
100-
{
101-
var reportedMetrics = meteringClasses
102-
.Select(meteringClass => new ReportedMetricClass(
103-
Name: meteringClass.Name,
104-
RootNamespace: rootNamespace ?? meteringClass.Namespace,
105-
Constraints: meteringClass.Constraints,
106-
Modifiers: meteringClass.Modifiers,
107-
Methods: meteringClass.Methods.Select(meteringMethod => new ReportedMetricMethod(
108-
MetricName: meteringMethod.MetricName ?? "(Missing Name)",
109-
Summary: meteringMethod.XmlDefinition ?? "(Missing Summary)",
110-
Kind: meteringMethod.InstrumentKind,
111-
Dimensions: meteringMethod.TagKeys,
112-
DimensionsDescriptions: meteringMethod.TagDescriptionDictionary))
113-
.ToArray()));
114-
115-
return reportedMetrics.ToArray();
116-
}
11787
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using Microsoft.Gen.Metrics.Model;
7+
8+
namespace Microsoft.Gen.MetricsReports;
9+
internal static class MetricsReportsHelpers
10+
{
11+
internal static ReportedMetricClass[] MapToCommonModel(IReadOnlyList<MetricType> meteringClasses, string? rootNamespace)
12+
{
13+
var reportedMetrics = meteringClasses
14+
.Select(meteringClass => new ReportedMetricClass(
15+
Name: meteringClass.Name,
16+
RootNamespace: rootNamespace ?? meteringClass.Namespace,
17+
Constraints: meteringClass.Constraints,
18+
Modifiers: meteringClass.Modifiers,
19+
Methods: meteringClass.Methods.Select(meteringMethod => new ReportedMetricMethod(
20+
MetricName: meteringMethod.MetricName ?? "(Missing Name)",
21+
Summary: meteringMethod.XmlDefinition ?? "(Missing Summary)",
22+
Kind: meteringMethod.InstrumentKind,
23+
Dimensions: meteringMethod.TagKeys,
24+
DimensionsDescriptions: meteringMethod.TagDescriptionDictionary))
25+
.ToArray()));
26+
27+
return reportedMetrics.ToArray();
28+
}
29+
}

src/Packages/Microsoft.Extensions.AuditReports/Microsoft.Extensions.AuditReports.csproj

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
<MinMutationScore>n/a</MinMutationScore>
1515
</PropertyGroup>
1616

17+
1718
<ItemGroup>
18-
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.ComplianceReports\Microsoft.Gen.ComplianceReports.csproj" />
19-
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.MetricsReports\Microsoft.Gen.MetricsReports.csproj" />
19+
<None Include="buildTransitive\*" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="buildTransitive" />
2020
</ItemGroup>
2121

22+
2223
<ItemGroup>
23-
<None Include="buildTransitive\*" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="buildTransitive" />
24+
<ProjectReference Include="..\..\Generators\Microsoft.Gen.MetadataExtractor\Microsoft.Gen.MetadataExtractor.csproj" />
2425
</ItemGroup>
2526

2627
</Project>

0 commit comments

Comments
 (0)