Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
#nullable enable
namespace NServiceBus.AzureFunctions.Analyzer;

using System.Security.Cryptography;
using System.Text;
using Microsoft.CodeAnalysis;
using Utility;

record struct CompilationAssemblyDetails(string SimpleName, string Identity)
readonly record struct CompilationAssemblyDetails(string SimpleName, string Identity)
{
public static CompilationAssemblyDetails FromAssembly(IAssemblySymbol assembly) => new(assembly.Name, assembly.Identity.GetDisplayName());

const string NamePrefix = "GeneratedFunctionRegistrations_";
const int HashBytesToUse = 10;

public readonly string ToGenerationClassName()
public string ToGenerationClassName()
{
var sb = new StringBuilder(NamePrefix, NamePrefix.Length + SimpleName.Length + 1 + (HashBytesToUse * 2))
.Append(SimpleName.Replace('.', '_'))
.Append('_');

using var sha = SHA256.Create();

var identityBytes = Encoding.UTF8.GetBytes(Identity);
var hashBytes = sha.ComputeHash(identityBytes);
for (var i = 0; i < HashBytesToUse; i++)
{
_ = sb.Append(hashBytes[i].ToString("x2"));
}

return sb.ToString();
var hash = NonCryptographicHash.GetHash(Identity);
return $"{NamePrefix}{SimpleName.Replace('.', '_')}_{hash:x16}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace NServiceBus.AzureFunctions.Analyzer;

using Microsoft.CodeAnalysis;
using Utility;

public sealed partial class FunctionCompositionGenerator
{
static class Emitter
{
public static void Emit(SourceProductionContext context, CompositionSpec? composition)
{
context.CancellationToken.ThrowIfCancellationRequested();

if (composition is null)
{
return;
}

var writer = new SourceWriter();
writer.PreAmble();
writer.WithOpenNamespace(composition.RootNamespace);
writer.WriteLine("public static class NServiceBusFunctionsComposition");
writer.WriteLine("{");
writer.Indentation++;
writer.WriteLine("extension(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder)");
writer.WriteLine("{");
writer.Indentation++;
writer.WriteLine("public void AddNServiceBusFunctions()");
writer.WriteLine("{");
writer.Indentation++;
writer.WriteLine("global::System.ArgumentNullException.ThrowIfNull(builder);");
writer.WriteLine("global::NServiceBus.NServiceBusFunctionsInfrastructure.Initialize(builder);");
writer.WriteLine();

foreach (var registrationClass in composition.RegistrationClasses)
{
context.CancellationToken.ThrowIfCancellationRequested();
writer.WriteLine($"foreach (var manifest in global::{registrationClass.FullClassName}.GetFunctionManifests())");
writer.WriteLine(" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction(builder, manifest);");
}

writer.CloseCurlies();
context.AddSource(TrackingNames.Composition, writer.ToSourceText());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#nullable enable
namespace NServiceBus.AzureFunctions.Analyzer;

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Core.Analyzer;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

public sealed partial class FunctionCompositionGenerator
{
static class Parser
{
internal static HostProjectSpec ParseHostProject(AnalyzerConfigOptionsProvider provider)
{
provider.GlobalOptions.TryGetValue("build_property.OutputType", out var outputType);
provider.GlobalOptions.TryGetValue("build_property.AzureFunctionsVersion", out var azureFunctionsVersion);
provider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace);
var isHostProject = string.Equals(outputType, "Exe", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(azureFunctionsVersion);
string? effectiveRootNameSpace = string.IsNullOrWhiteSpace(rootNamespace) ? null : rootNamespace;

return new HostProjectSpec(isHostProject, effectiveRootNameSpace);
}

internal static CompositionSpec? ParseComposition(Compilation compilation, HostProjectSpec hostProject, CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

if (!hostProject.IsHostProject)
{
return null;
}

var registrations = new HashSet<GeneratedRegistrationClassSpec>();

foreach (var referencedAssembly in compilation.SourceModule.ReferencedAssemblySymbols)
{
cancellationToken.ThrowIfCancellationRequested();

var registration = CreateGeneratedRegistrationClassSpec(referencedAssembly);
if (compilation.GetTypeByMetadataName(registration.FullClassName) is not null)
{
registrations.Add(registration);
}
}

registrations.Add(CreateGeneratedRegistrationClassSpec(compilation.Assembly));

var orderedRegistrations = registrations
.OrderBy(static registration => registration.FullClassName, StringComparer.Ordinal)
.ToImmutableEquatableArray();

return new CompositionSpec(orderedRegistrations, hostProject.RootNamespace);
}

static GeneratedRegistrationClassSpec CreateGeneratedRegistrationClassSpec(IAssemblySymbol assembly)
{
var className = CompilationAssemblyDetails.FromAssembly(assembly).ToGenerationClassName();
return new GeneratedRegistrationClassSpec($"NServiceBus.Generated.{className}");
}
}

internal readonly record struct GeneratedRegistrationClassSpec(string FullClassName);
internal readonly record struct HostProjectSpec(bool IsHostProject, string? RootNamespace);
internal sealed record CompositionSpec(ImmutableEquatableArray<GeneratedRegistrationClassSpec> RegistrationClasses, string? RootNamespace);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace NServiceBus.AzureFunctions.Analyzer;

public sealed partial class FunctionCompositionGenerator
{
static class TrackingNames
{
public const string HostProject = nameof(HostProject);
public const string Composition = nameof(Composition);
}
}
119 changes: 13 additions & 106 deletions src/NServiceBus.AzureFunctions.Analyzer/FunctionCompositionGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,116 +1,23 @@
#nullable enable
namespace NServiceBus.AzureFunctions.Analyzer;

using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

[Generator]
public sealed class FunctionCompositionGenerator : IIncrementalGenerator
public sealed partial class FunctionCompositionGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var hostProjectInfo = context.AnalyzerConfigOptionsProvider.Select((provider, _) =>
{
provider.GlobalOptions.TryGetValue("build_property.OutputType", out var outputType);
provider.GlobalOptions.TryGetValue("build_property.AzureFunctionsVersion", out var azureFunctionsVersion);
provider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var rootNamespace);

var isHost = string.Equals(outputType, "Exe", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(azureFunctionsVersion);

return new HostProjectInfo(isHost, rootNamespace);
});

var registrationTypesFromReferences = context.CompilationProvider.SelectMany((compilation, ct) =>
{
var results = ImmutableArray.CreateBuilder<GeneratedRegistrationClass>();
foreach (var assembly in compilation.SourceModule.ReferencedAssemblySymbols)
{
ct.ThrowIfCancellationRequested();
var className = CompilationAssemblyDetails.FromAssembly(assembly).ToGenerationClassName();
var fullName = $"NServiceBus.Generated.{className}";
if (compilation.GetTypeByMetadataName(fullName) is not null)
{
results.Add(new GeneratedRegistrationClass(fullName));
}
}
return results.ToImmutable();
});

var collectedRegistrationTypes = registrationTypesFromReferences.Collect();

var currentAssemblyRegistrationTypeByConvention = context.CompilationProvider
.Select((compilation, _) => CompilationAssemblyDetails.FromAssembly(compilation.Assembly))
.Select((assemblyInfo, _) => assemblyInfo.ToGenerationClassName())
.Select((className, _) => new GeneratedRegistrationClass($"NServiceBus.Generated.{className}"));

var allData = collectedRegistrationTypes
.Combine(hostProjectInfo)
.Combine(currentAssemblyRegistrationTypeByConvention)
.Select((tuple, _) =>
{
var ((regClasses, hostInfo), currentAssemblyClassName) = tuple;

if (!hostInfo.IsHost)
{
return default;
}

var allClasses = regClasses
.Concat([currentAssemblyClassName])
.Distinct()
.ToImmutableArray();

return new CompositionData(allClasses, hostInfo.RootNamespace);
});

context.RegisterSourceOutput(allData, GenerateCompositionCode);
var hostProject = context.AnalyzerConfigOptionsProvider
.Select(static (provider, _) => Parser.ParseHostProject(provider))
.WithTrackingName(TrackingNames.HostProject);

var compositions = context.CompilationProvider
.Combine(hostProject)
.Select(static (data, cancellationToken) => Parser.ParseComposition(data.Left, data.Right, cancellationToken))
.WithTrackingName(TrackingNames.Composition);

context.RegisterSourceOutput(
compositions,
static (context, composition) => Emitter.Emit(context, composition));
}

static void GenerateCompositionCode(SourceProductionContext spc, CompositionData data)
{
if (data == default)
{
return;
}

var regClasses = data.RegistrationClasses;
var ns = data.RootNamespace;

var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable");
sb.AppendLine();
sb.AppendLine($"namespace {ns};");
sb.AppendLine();
sb.AppendLine("public static class NServiceBusFunctionsComposition");
sb.AppendLine("{");
sb.AppendLine(" extension(global::Microsoft.Azure.Functions.Worker.Builder.FunctionsApplicationBuilder builder)");
sb.AppendLine(" {");
sb.AppendLine(" public void AddNServiceBusFunctions()");
sb.AppendLine(" {");
sb.AppendLine(" global::System.ArgumentNullException.ThrowIfNull(builder);");
sb.AppendLine(" global::NServiceBus.NServiceBusFunctionsInfrastructure.Initialize(builder);");
sb.AppendLine();

foreach (var regClass in regClasses)
{
sb.AppendLine($" foreach (var m in global::{regClass.FullClassName}.GetFunctionManifests())");
sb.AppendLine($" global::NServiceBus.FunctionsHostApplicationBuilderExtensions.AddNServiceBusFunction(builder, m);");
}
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.Append("}");

spc.AddSource("Composition.g.cs", sb.ToString());
}

record struct GeneratedRegistrationClass(string FullClassName);
record struct HostProjectInfo(bool IsHost, string? RootNamespace);
record struct CompositionData(
ImmutableArray<GeneratedRegistrationClass> RegistrationClasses,
string? RootNamespace);

}
Loading
Loading