Skip to content

Commit

Permalink
Metadata reports generator (Issue #3999) (#5531)
Browse files Browse the repository at this point in the history
Metadata reports generator (Issue #3999) (#5531)
  • Loading branch information
IbrahimMNada authored Jan 27, 2025
1 parent 934f767 commit fc84cf9
Show file tree
Hide file tree
Showing 26 changed files with 1,940 additions and 46 deletions.
8 changes: 6 additions & 2 deletions src/Generators/Microsoft.Gen.ComplianceReports/Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClassifiedType> classifiedTypes, string assemblyName)
public string Emit(IReadOnlyCollection<ClassifiedType> 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", () =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Generates reports for compliance & metrics annotations.
/// </summary>
[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;

/// <summary>
/// Initializes a new instance of the <see cref="MetadataReportsGenerator"/> class.
/// </summary>
public MetadataReportsGenerator()
: this(FallbackFileName)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="MetadataReportsGenerator"/> class.
/// </summary>
/// <param name="reportFileName">The report file name.</param>
public MetadataReportsGenerator(string reportFileName)
{
_fileName = reportFileName;
}

/// <summary>
/// Initializes the generator.
/// </summary>
/// <param name="context">The generator initialization context.</param>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(TypeDeclarationSyntaxReceiver.Create);
}

/// <summary>
/// Generates reports for compliance & metrics annotations.
/// </summary>
/// <param name="context">The generator execution context.</param>
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 <MetadataReportOutputPath> and <OutputPath> 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

}

/// <summary>
/// used to generate the report for metrics annotations.
/// </summary>
/// <param name="context">The generator execution context.</param>
/// <param name="receiver">The typeDeclaration syntax receiver.</param>
/// <returns>string report as json or String.Empty.</returns>
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;
}

/// <summary>
/// used to generate the report for compliance annotations.
/// </summary>
/// <param name="context">The generator execution context.</param>
/// <param name="receiver">The type declaration syntax receiver.</param>
/// <returns>string report as json or String.Empty.</returns>
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>Microsoft.Gen.MetadataExtractor</RootNamespace>
<Description>Produces compliance and metrics reports based on data classification annotations in the code.</Description>
<Workstream>Fundamentals</Workstream>
</PropertyGroup>

<PropertyGroup>
<AnalyzerLanguage>cs</AnalyzerLanguage>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
</PropertyGroup>

<PropertyGroup>
<Stage>dev</Stage>
<MinCodeCoverage>98</MinCodeCoverage>
<MinMutationScore>85</MinMutationScore>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Shared\TypeDeclarationSyntaxReceiver.cs" LinkBase="Shared" />
<Compile Include="..\Shared\GeneratorUtilities.cs" LinkBase="Shared" />
<Compile Include="..\Shared\ClassDeclarationSyntaxReceiver.cs" LinkBase="Shared" />
<Compile Include="..\Shared\EmitterBase.cs" LinkBase="Shared" />
<Compile Include="..\Shared\ParserUtilities.cs" LinkBase="Shared" />
<Compile Include="..\Shared\DiagDescriptorsBase.cs" LinkBase="Shared" />
<Compile Include="..\Shared\StringBuilderPool.cs" LinkBase="Shared" />
<Compile Include="..\Microsoft.Gen.ComplianceReports\Model\*.cs" LinkBase="Microsoft.Gen.ComplianceReports" />
<Compile Include="..\Microsoft.Gen.ComplianceReports\*.cs" LinkBase="Microsoft.Gen.ComplianceReports" />
<Compile Include="..\Microsoft.Gen.Metrics\Exceptions\*.cs" LinkBase="Microsoft.Gen.Metrics" />
<Compile Include="..\Microsoft.Gen.MetricsReports\*.cs" LinkBase="Microsoft.Gen.MetricsReports" />
<Compile Include="..\Microsoft.Gen.Metrics\Model\*.cs" LinkBase="Microsoft.Gen.Metrics" />
<Compile Include="..\Microsoft.Gen.Metrics\*.cs" LinkBase="Microsoft.Gen.Metrics" />
<Compile Include="..\Microsoft.Gen.Shared\*.cs" LinkBase="Microsoft.Gen.Metrics" />
<Compile Include="..\Microsoft.Gen.Shared\*.cs" LinkBase="Microsoft.Gen.Metrics" />



</ItemGroup>

<ItemGroup>
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.ComplianceReports\Microsoft.Gen.ComplianceReports.csproj" />
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.MetricsReports\Microsoft.Gen.MetricsReports.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Gen.Metrics\Microsoft.Gen.Metrics.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleToTest Include="Microsoft.Gen.MetadataExtractor.Unit.Tests" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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()
Expand All @@ -42,23 +37,13 @@ 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))
{
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)
Expand All @@ -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
Expand All @@ -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<MetricType> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<MetricType> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
<MinMutationScore>n/a</MinMutationScore>
</PropertyGroup>


<ItemGroup>
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.ComplianceReports\Microsoft.Gen.ComplianceReports.csproj" />
<AnalyzerReference Include="..\..\Generators\Microsoft.Gen.MetricsReports\Microsoft.Gen.MetricsReports.csproj" />
<None Include="buildTransitive\*" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="buildTransitive" />
</ItemGroup>


<ItemGroup>
<None Include="buildTransitive\*" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="buildTransitive" />
<ProjectReference Include="..\..\Generators\Microsoft.Gen.MetadataExtractor\Microsoft.Gen.MetadataExtractor.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit fc84cf9

Please sign in to comment.