Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metadata reports generator (Issue #3999) #5531

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
78e08b5
adding MetadataReportsGenerator
IbrahimMNada Oct 17, 2024
51d6362
fix format
IbrahimMNada Oct 17, 2024
37bf450
fix refs
IbrahimMNada Oct 17, 2024
9f3cb7d
revert dotnet version
IbrahimMNada Oct 17, 2024
d448547
remove unneeded using spaces
IbrahimMNada Oct 17, 2024
235aa18
remove un needed usings
IbrahimMNada Oct 17, 2024
54b2eae
manual testing
IbrahimMNada Oct 17, 2024
4581b20
adding test project
IbrahimMNada Oct 17, 2024
2eadb5c
fix order for tests
IbrahimMNada Oct 17, 2024
34a9f7e
adding test classes
IbrahimMNada Oct 17, 2024
12f8a24
extra testing class
IbrahimMNada Oct 17, 2024
fff7be2
adding test methods
IbrahimMNada Oct 17, 2024
a9bf590
methods
IbrahimMNada Oct 17, 2024
6709e78
adding testng methods
IbrahimMNada Oct 17, 2024
b72031d
adding tests
IbrahimMNada Oct 17, 2024
e315855
finshing tests
IbrahimMNada Oct 17, 2024
0e63552
remove duplication
IbrahimMNada Oct 17, 2024
ff82288
multi blanlines removed
IbrahimMNada Oct 17, 2024
bfbab5b
fixing format
IbrahimMNada Oct 18, 2024
7a6abb6
add comments
IbrahimMNada Oct 18, 2024
197bdb9
adding comment for methods
IbrahimMNada Oct 18, 2024
b08ad67
remove multi lines
IbrahimMNada Oct 18, 2024
da8a021
run tests & adding comments
IbrahimMNada Oct 18, 2024
61d7767
adding comments
IbrahimMNada Oct 18, 2024
5c5f2e3
remove blanks
IbrahimMNada Oct 18, 2024
8384771
Merge branch 'main' into MetadataReportsGenerator
IbrahimMNada Oct 27, 2024
8bfcefa
Merge branch 'main' into MetadataReportsGenerator
IbrahimMNada Nov 5, 2024
cbca87e
Merge branch 'main' into MetadataReportsGenerator
IbrahimMNada Nov 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added because you have a new reported dependent on this , and Compliance report is only a part of that bigger report, which have the assemblyName at the very top of it.

so we dont need to replicate the assemblyName in the properties.

The value is defaulted to true, to avoid any breaking changes for backward computability

{
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>normal</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
Loading