Skip to content

Commit

Permalink
Fix Microsoft.Extensions.AuditReports output path (#4945)
Browse files Browse the repository at this point in the history
* Fix Microsoft.Extensions.AuditReports output path
* fix golden reports
* add tests and the README
  • Loading branch information
xakep139 committed Mar 5, 2024
1 parent 76b6d4a commit 52350a9
Show file tree
Hide file tree
Showing 19 changed files with 492 additions and 3,933 deletions.
8 changes: 7 additions & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ if desired.
| `EXTEXP0016` | Hosting integration testing experiments |
| `EXTEXP0017` | Contextual options experiments |


# LoggerMessage

| Diagnostic ID | Description |
Expand Down Expand Up @@ -108,3 +107,10 @@ if desired.
| `METGEN017` | Gauge is not supported yet |
| `METGEN018` | Xml comment was not parsed correctly |
| `METGEN019` | A metric class has cycles in its type hierarchy |

## AuditReports

| Diagnostic ID | Description |
| :---------------- | :---------- |
| `AUDREPGEN000` | MetricsReports generator couldn't resolve output path for the report |
| `AUDREPGEN001` | ComplianceReports generator couldn't resolve output path for the report |
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"sdk": {
"version": "8.0.101"
"version": "8.0.200"
},
"tools": {
"dotnet": "8.0.101",
"dotnet": "8.0.200",
"runtimes": {
"dotnet/x64": [
"6.0.22"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +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 System.Globalization;
using System.IO;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.Gen.Shared;
using Microsoft.Shared.DiagnosticIds;

namespace Microsoft.Gen.ComplianceReports;

Expand All @@ -18,8 +21,8 @@ public sealed class ComplianceReportsGenerator : ISourceGenerator

private const string FallbackFileName = "ComplianceReport.json";

private readonly string _fileName;
private string? _directory;
private string _fileName;

public ComplianceReportsGenerator()
: this(null)
Expand Down Expand Up @@ -55,7 +58,7 @@ public void Execute(GeneratorExecutionContext context)

if (!GeneratorUtilities.ShouldGenerateReport(context, GenerateComplianceReportsMSBuildProperty))
{
// By default, compliance reports are only generated only during build time and not during design time to prevent the file being written on every keystroke in VS.
// By default, compliance reports are generated only during build time and not during design time to prevent the file being written on every keystroke in VS.
return;
}

Expand All @@ -78,19 +81,30 @@ public void Execute(GeneratorExecutionContext context)

context.CancellationToken.ThrowIfCancellationRequested();

if (_directory == null)
var options = context.AnalyzerConfigOptions.GlobalOptions;
_directory ??= GeneratorUtilities.TryRetrieveOptionsValue(options, ComplianceReportOutputPathMSBuildProperty, out var reportOutputPath)
? reportOutputPath!
: GeneratorUtilities.GetDefaultReportOutputPath(options);

if (string.IsNullOrWhiteSpace(_directory))
{
_ = context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(ComplianceReportOutputPathMSBuildProperty, out _directory);
if (string.IsNullOrWhiteSpace(_directory))
{
// no valid output path
return;
}
// Report diagnostic:
var diagnostic = new DiagnosticDescriptor(
DiagnosticIds.AuditReports.AUDREPGEN001,
"ComplianceReports generator couldn't resolve output path for the report. It won't be generated.",
"Both <ComplianceReportOutputPath> 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.AUDREPGEN001));

context.ReportDiagnostic(Diagnostic.Create(diagnostic, location: null));
return;
}

_ = Directory.CreateDirectory(_directory);

// Write report as JSON file.
File.WriteAllText(Path.Combine(_directory, _fileName), report);
File.WriteAllText(Path.Combine(_directory, _fileName), report, Encoding.UTF8);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// 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;

namespace Microsoft.Gen.MetricsReports;

Expand All @@ -18,13 +20,19 @@ public class MetricsReportsGenerator : ISourceGenerator
private const string GenerateMetricDefinitionReport = "build_property.GenerateMetricsReport";
private const string RootNamespace = "build_property.rootnamespace";
private const string ReportOutputPath = "build_property.MetricsReportOutputPath";
private const string CompilationOutputPath = "build_property.outputpath";
private const string CurrentProjectPath = "build_property.projectdir";
private const string FileName = "MetricsReport.json";

private string? _compilationOutputPath;
private string? _currentProjectPath;
private string? _reportOutputPath;
private readonly string _fileName;

public MetricsReportsGenerator()
: this(FileName)
{
}

internal MetricsReportsGenerator(string reportFileName)
{
_fileName = reportFileName;
}

public void Initialize(GeneratorInitializationContext context)
{
Expand All @@ -35,8 +43,9 @@ public void Execute(GeneratorExecutionContext context)
{
context.CancellationToken.ThrowIfCancellationRequested();

var receiver = context.SyntaxReceiver as ClassDeclarationSyntaxReceiver;
if (receiver == null || receiver.ClassDeclarations.Count == 0 || !GeneratorUtilities.ShouldGenerateReport(context, GenerateMetricDefinitionReport))
if (context.SyntaxReceiver is not ClassDeclarationSyntaxReceiver receiver ||
receiver.ClassDeclarations.Count == 0 ||
!GeneratorUtilities.ShouldGenerateReport(context, GenerateMetricDefinitionReport))
{
return;
}
Expand All @@ -52,13 +61,24 @@ public void Execute(GeneratorExecutionContext context)

var options = context.AnalyzerConfigOptions.GlobalOptions;

var path = (_reportOutputPath != null || options.TryGetValue(ReportOutputPath, out _reportOutputPath))
? _reportOutputPath
: GetDefaultReportOutputPath(options);
var path = GeneratorUtilities.TryRetrieveOptionsValue(options, ReportOutputPath, out var reportOutputPath)
? reportOutputPath!
: GeneratorUtilities.GetDefaultReportOutputPath(options);

if (string.IsNullOrWhiteSpace(path))
{
// Report diagnostic. Tell that it is either <MetricDefinitionReportOutputPath> missing or <CompilerVisibleProperty Include="OutputPath"/> visibility to compiler.
// 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 <MetricsReportOutputPath> 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;
}

Expand All @@ -68,7 +88,7 @@ public void Execute(GeneratorExecutionContext context)
var reportedMetrics = MapToCommonModel(meteringClasses, rootNamespace);
var report = emitter.GenerateReport(reportedMetrics, context.CancellationToken);

File.WriteAllText(Path.Combine(path, FileName), report, Encoding.UTF8);
File.WriteAllText(Path.Combine(path, _fileName), report, Encoding.UTF8);
}

private static ReportedMetricClass[] MapToCommonModel(IReadOnlyList<MetricType> meteringClasses, string? rootNamespace)
Expand All @@ -89,19 +109,4 @@ private static ReportedMetricClass[] MapToCommonModel(IReadOnlyList<MetricType>

return reportedMetrics.ToArray();
}

private string GetDefaultReportOutputPath(AnalyzerConfigOptions options)
{
if (_currentProjectPath != null && _compilationOutputPath != null)
{
return _currentProjectPath + _compilationOutputPath;
}

_ = options.TryGetValue(CompilationOutputPath, out _compilationOutputPath);
_ = options.TryGetValue(CurrentProjectPath, out _currentProjectPath);

return string.IsNullOrWhiteSpace(_currentProjectPath) || string.IsNullOrWhiteSpace(_compilationOutputPath)
? string.Empty
: _currentProjectPath + _compilationOutputPath;
}
}
27 changes: 27 additions & 0 deletions src/Generators/Shared/GeneratorUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[assembly: System.Resources.NeutralResourcesLanguage("en-us")]

Expand All @@ -21,6 +23,9 @@ namespace Microsoft.Gen.Shared;
#endif
internal static class GeneratorUtilities
{
private const string CompilationOutputPath = "build_property.outputpath";
private const string CurrentProjectPath = "build_property.projectdir";

public static string GeneratedCodeAttribute { get; } = $"global::System.CodeDom.Compiler.GeneratedCodeAttribute(" +
$"\"{typeof(GeneratorUtilities).Assembly.GetName().Name}\", " +
$"\"{typeof(GeneratorUtilities).Assembly.GetName().Version}\")";
Expand Down Expand Up @@ -136,4 +141,26 @@ public static bool ShouldGenerateReport(GeneratorExecutionContext context, strin

return string.Equals(generateFiles, bool.TrueString, StringComparison.OrdinalIgnoreCase);
}

public static bool TryRetrieveOptionsValue(AnalyzerConfigOptions options, string name, out string? value)
=> options.TryGetValue(name, out value) && !string.IsNullOrWhiteSpace(value);

public static string GetDefaultReportOutputPath(AnalyzerConfigOptions options)
{
if (!TryRetrieveOptionsValue(options, CompilationOutputPath, out var compilationOutputPath))
{
return string.Empty;
}

// If <OutputPath> is absolute - return it right away:
if (Path.IsPathRooted(compilationOutputPath))
{
return compilationOutputPath!;
}

// Get <ProjectDir> and combine it with <OutputPath> if the former isn't empty:
return TryRetrieveOptionsValue(options, CurrentProjectPath, out var currentProjectPath)
? Path.Combine(currentProjectPath!, compilationOutputPath!)
: string.Empty;
}
}
Loading

0 comments on commit 52350a9

Please sign in to comment.