Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion Lagrange.Core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
<Project Path="Lagrange.Proto/Lagrange.Proto.csproj" />
</Folder>
<Folder Name="/Protocols.Milky/">
<Project Path="Lagrange.Milky.Implementation.Api.Generator/Lagrange.Milky.Implementation.Api.Generator.csproj" />
<Project Path="Lagrange.Milky/Lagrange.Milky.csproj" />
<Project Path="Lagrange.Milky.Generator/Lagrange.Milky.Generator.csproj" />
<!-- <Project Path="Lagrange.Milky.Implementation.Api.Generator/Lagrange.Milky.Implementation.Api.Generator.csproj" /> -->
</Folder>
<Folder Name="/Solution Items/">
<File Path="Proto.md" />
Expand Down
24 changes: 24 additions & 0 deletions Lagrange.Milky.Generator/Api/ApiDiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.CodeAnalysis;

namespace Lagrange.Milky.Generator.Api;

public class ApiDiagnosticDescriptors
{
public static readonly DiagnosticDescriptor MustImplementInterfaceError = new(
id: "LMA001",
title: "ApiHandler attribute usage error",
messageFormat: "Type '{0}' marked with ApiHandlerAttribute must implement IApiHandler<TRequest, TResult> or INoRequestApiHandler<TResult> or INoResultApiHandler<TRequest>",
category: "Lagrange.Milky.Api",
DiagnosticSeverity.Error,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DuplicateApiNameError = new(
id: "LMA002",
title: "Duplicate api name",
messageFormat: "Type '{0}' cannot be used as a handler with name '{1}' because this name is already associated with another handler",
category: "Lagrange.Milky.Api",
DiagnosticSeverity.Error,
isEnabledByDefault: true
);
}
128 changes: 128 additions & 0 deletions Lagrange.Milky.Generator/Api/ApiExtensionGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
using Lagrange.Milky.Generator.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Lagrange.Milky.Generator.Api;

[Generator(LanguageNames.CSharp)]
public class ApiExtensionGenerator : IIncrementalGenerator
{
private const string ApiHandlerAttributeTypeName = "Lagrange.Milky.Api.Attributes.ApiHandlerAttribute";
private const string IApiHandlerTypeName = "Lagrange.Milky.Api.Handlers.IApiHandler`2";
private const string INoRequestApiHandlerTypeName = "Lagrange.Milky.Api.Handlers.INoRequestApiHandler`1";
private const string INoResultApiHandlerTypeName = "Lagrange.Milky.Api.Handlers.INoResultApiHandler`1";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var handlerInfos = context.SyntaxProvider.ForAttributeWithMetadataName(
ApiHandlerAttributeTypeName,
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: GetHandleInfo
).Collect();

context.RegisterSourceOutput(
handlerInfos,
static (context, handlerInfos) =>
{
var validHandlerInfos = FilterHandlerInfoAndReportDiagnostics(context, handlerInfos);

string generatedCode = GenerateSourceCode(validHandlerInfos);
context.AddSource("Lagrange.Milky.Api.Extensions.EventExtension.g.cs", generatedCode);
}
);
}

private (string, string, int, bool, Location?) GetHandleInfo(GeneratorAttributeSyntaxContext context, CancellationToken token)
{
var compilation = context.SemanticModel.Compilation;
var handlerSymbol = (INamedTypeSymbol)context.TargetSymbol;

string handlerTypeName = handlerSymbol.ToDisplayString();

var attributeData = context.Attributes.First();
string name = (string)attributeData.ConstructorArguments
.First()
.Value!;
int lifetime = (int?)attributeData
.NamedArguments
.FirstOrDefault(a => a.Key == "Lifetime")
.Value
.Value
?? 0;

var handlerInterfaceSymbol = compilation.GetTypeByMetadataName(IApiHandlerTypeName);
var noRequestHandlerInterfaceSymbol = compilation.GetTypeByMetadataName(INoRequestApiHandlerTypeName);
var noResultHandlerInterfaceSymbol = compilation.GetTypeByMetadataName(INoResultApiHandlerTypeName);
bool isImpl = handlerSymbol.AllInterfaces
.Any(s => s.OriginalDefinition.DefaultEquals(handlerInterfaceSymbol)
|| s.OriginalDefinition.DefaultEquals(noRequestHandlerInterfaceSymbol)
|| s.OriginalDefinition.DefaultEquals(noResultHandlerInterfaceSymbol));

var location = handlerSymbol.Locations.FirstOrDefault();

return (handlerTypeName, name, lifetime, isImpl, location);
}

private static IEnumerable<(string, string, int)> FilterHandlerInfoAndReportDiagnostics(SourceProductionContext context, ImmutableArray<(string, string, int, bool, Location?)> handlerInfos)
{
var apiNames = new HashSet<string>(StringComparer.Ordinal);
var result = new List<(string, string, int)>();

foreach ((string handlerTypeName, string apiName, int lifetime, bool isImpl, var location) in handlerInfos)
{
if (!isImpl)
{
context.ReportDiagnostic(Diagnostic.Create(
ApiDiagnosticDescriptors.MustImplementInterfaceError,
location,
handlerTypeName
));
continue;
}

if (!apiNames.Add(apiName))
{
context.ReportDiagnostic(Diagnostic.Create(
ApiDiagnosticDescriptors.DuplicateApiNameError,
location,
handlerTypeName,
apiName
));
continue;
}

result.Add((handlerTypeName, apiName, lifetime));
}

return result;
}

private static string GenerateSourceCode(IEnumerable<(string, string, int)> handlerInfos)
{
var builder = new StringBuilder();

builder.AppendLine("// <auto-generated/>");
builder.AppendLine();
builder.AppendLine("namespace Lagrange.Milky.Api.Extensions;");
builder.AppendLine();
builder.AppendLine("public static partial class ApiExtension");
builder.AppendLine("{");
builder.AppendLine(" public static partial Microsoft.Extensions.DependencyInjection.IServiceCollection AddApiHandlers(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)");
builder.AppendLine(" {");
foreach (var (handlerName, apiName, lifetime) in handlerInfos)
{
builder.AppendLine($" services.Add(new Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(Lagrange.Milky.Api.Handlers.IApiHandler), \"{apiName}\", typeof({handlerName}), (Microsoft.Extensions.DependencyInjection.ServiceLifetime){lifetime}));");
}
builder.AppendLine(" return services;");
builder.AppendLine(" }");
builder.AppendLine("}");

return builder.ToString();
}
}
24 changes: 24 additions & 0 deletions Lagrange.Milky.Generator/Events/EventDiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.CodeAnalysis;

namespace Lagrange.Milky.Generator.Events;

public class EventDiagnosticDescriptors
{
public static readonly DiagnosticDescriptor MustImplementInterfaceError = new(
id: "LME001",
title: "EventSerializer attribute usage error",
messageFormat: "Type '{0}' marked with EventSerializerAttribute must implement IEventSerializer<TEvent, TData>",
category: "Lagrange.Milky.Events",
DiagnosticSeverity.Error,
isEnabledByDefault: true
);

public static readonly DiagnosticDescriptor DuplicateEventSerializerError = new(
id: "LME002",
title: "Duplicate event serializer",
messageFormat: "Type '{1}' (priority {2}) cannot be used as a serializer for event '{0}' because this event is already associated with another serializer",
category: "Lagrange.Milky.Events",
DiagnosticSeverity.Error,
isEnabledByDefault: true
);
}
146 changes: 146 additions & 0 deletions Lagrange.Milky.Generator/Events/EventExtensionGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading;
using Lagrange.Milky.Generator.Extensions;
using Lagrange.Milky.Generator.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Lagrange.Milky.Generator.Events;

[Generator(LanguageNames.CSharp)]
public class EventExtensionGenerator : IIncrementalGenerator
{
private const string EventConverterAttributeTypeName = "Lagrange.Milky.Events.Attributes.EventConverterAttribute";
private const string IEventConverterTypeName = "Lagrange.Milky.Events.Converters.IEventConverter`2";

public void Initialize(IncrementalGeneratorInitializationContext context)
{
var converterInfos = context.SyntaxProvider.ForAttributeWithMetadataName(
EventConverterAttributeTypeName,
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: GetConverterInfo
).Collect();

context.RegisterSourceOutput(
converterInfos,
static (context, converterInfos) =>
{
var validConverterInfos = FilterConverterInfoAndReportDiagnostics(context, converterInfos);

string generatedCode = GenerateSourceCode(validConverterInfos);
context.AddSource("Lagrange.Milky.Events.Extensions.EventExtension.g.cs", generatedCode);
}
);
}

private (string, string?, long, int, Location?) GetConverterInfo(GeneratorAttributeSyntaxContext context, CancellationToken ct)
{
var compilation = context.SemanticModel.Compilation;
var converterSymbol = (INamedTypeSymbol)context.TargetSymbol;

string converterTypeName = converterSymbol.ToDisplayString();

var namedArguments = context.Attributes.First().NamedArguments;
long priority = (long?)namedArguments
.FirstOrDefault(a => a.Key == "Priority")
.Value
.Value
?? 0;
int lifetime = (int?)namedArguments
.FirstOrDefault(a => a.Key == "Lifetime")
.Value
.Value
?? 0;

var converterInterfaceSymbol = compilation.GetTypeByMetadataName(IEventConverterTypeName);
string? eventTypeName = converterSymbol.AllInterfaces
.FirstOrDefault(s => s.OriginalDefinition.DefaultEquals(converterInterfaceSymbol))?
.TypeArguments
.FirstOrDefault()?
.ToDisplayString();

var location = converterSymbol.Locations.FirstOrDefault();

return (converterTypeName, eventTypeName, priority, lifetime, location);
}

private static IEnumerable<(string, string, long, int)> FilterConverterInfoAndReportDiagnostics(SourceProductionContext context, ImmutableArray<(string, string?, long, int, Location?)> converterInfos)
{
var eventTypeNames = new HashSet<(string, long)>(StringLongTupleComparer.Default);
var result = new List<(string, string, long, int)>();

foreach ((string? converterTypeName, string? eventTypeName, long priority, int lifetime, var location) in converterInfos)
{
if (eventTypeName is null)
{
context.ReportDiagnostic(Diagnostic.Create(
EventDiagnosticDescriptors.MustImplementInterfaceError,
location,
converterTypeName
));
continue;
}

if (!eventTypeNames.Add((eventTypeName, priority)))
{
context.ReportDiagnostic(Diagnostic.Create(
EventDiagnosticDescriptors.DuplicateEventSerializerError,
location,
eventTypeName,
converterTypeName,
priority
));
continue;
}

result.Add((converterTypeName, eventTypeName, priority, lifetime));
}

return result;
}

private static string GenerateSourceCode(IEnumerable<(string, string, long Priority, int)> converterInfos)
{
converterInfos = [.. converterInfos.OrderBy(i => i.Priority)];

var builder = new StringBuilder();

builder.AppendLine("// <auto-generated/>");
builder.AppendLine();
builder.AppendLine("namespace Lagrange.Milky.Events.Extensions;");
builder.AppendLine();
builder.AppendLine("public static partial class EventExtension");
builder.AppendLine("{");
builder.AppendLine(" public static partial void RegisterConvertibleEvents(this Lagrange.Core.BotContext lagrange, Lagrange.Milky.Events.IGenericEventHandler handler)");
builder.AppendLine(" {");
foreach ((_, string? eventTypeName, _, _) in converterInfos)
{
builder.AppendLine($" lagrange.EventInvoker.RegisterEvent<{eventTypeName}>(handler.OnEvent);");
}
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" public static partial void UnregisterConvertibleEvents(this Lagrange.Core.BotContext lagrange, Lagrange.Milky.Events.IGenericEventHandler handler)");
builder.AppendLine(" {");
foreach ((_, string? eventTypeName, _, _) in converterInfos)
{
builder.AppendLine($" lagrange.EventInvoker.UnregisterEvent<{eventTypeName}>(handler.OnEvent);");
}
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" public static partial Microsoft.Extensions.DependencyInjection.IServiceCollection AddEventConverters(this Microsoft.Extensions.DependencyInjection.IServiceCollection services)");
builder.AppendLine(" {");
foreach (var (converterName, eventName, _, lifetime) in converterInfos)
{
builder.AppendLine($" services.Add(new Microsoft.Extensions.DependencyInjection.ServiceDescriptor(typeof(Lagrange.Milky.Events.Converters.IEventConverter<{eventName}>), typeof({converterName}), (Microsoft.Extensions.DependencyInjection.ServiceLifetime){lifetime}));");
}
builder.AppendLine(" return services;");
builder.AppendLine(" }");
builder.AppendLine("}");

return builder.ToString();
}
}
11 changes: 11 additions & 0 deletions Lagrange.Milky.Generator/Extensions/SymbolExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.CodeAnalysis;

namespace Lagrange.Milky.Generator.Extensions;

public static class SymbolExtension
{
public static bool DefaultEquals(this ISymbol left, ISymbol? right)
{
return left.Equals(right, SymbolEqualityComparer.Default);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Nullable>enable</Nullable>
<LangVersion>12.0</LangVersion>
<LangVersion>14.0</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PkgMicrosoft_Bcl_HashCode)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="4.14.0">
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" GeneratePathProperty="true" PrivateAssets="all" ReferenceOutputAssembly="true" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
</ItemGroup>
</Project>
Loading
Loading