Skip to content
Draft
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
5 changes: 3 additions & 2 deletions .github/workflows/nuget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ jobs:
env:
REFERENCES_URL: https://exmod-team.github.io/SL-References/Dev.zip
REFERENCES_PATH: ${{ github.workspace }}/References
NUGET_PACKAGED_PATH: ${{ github.workspace }}/nupkgs

steps:
- name: Checkout
Expand All @@ -28,9 +29,9 @@ jobs:
- name: Build and Pack NuGet
env:
SL_REFERENCES: ${{ env.REFERENCES_PATH }}
run: dotnet pack -c Release --output ${GITHUB_WORKSPACE}/nupkgs
run: dotnet pack -c Release --output ${NUGET_PACKAGED_PATH}

- name: Push NuGet package
run: |
$PackageFile = (Get-ChildItem -Path "${GITHUB_WORKSPACE}/nupkgs" -Include 'SecretAPI.*.nupkg' -Recurse | Select-Object -First 1).FullName
$PackageFile = (Get-ChildItem -Path "${NUGET_PACKAGED_PATH}" -Include 'SecretAPI.*.nupkg' -Recurse | Select-Object -First 1).FullName
dotnet nuget push $PackageFile --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
7 changes: 3 additions & 4 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ jobs:
env:
SL_REFERENCES: ${{ env.REFERENCES_PATH }}
shell: pwsh
run: |
dotnet build -c Release
run: dotnet build -c Release

- name: Upload
uses: actions/upload-artifact@v4
with:
name: Build Result
path: ${{ github.workspace }}\SecretAPI\bin\Release\net48\SecretAPI.dll
retention-days: 7
path: ${{ github.workspace }}/**/bin/Release/net48/*SecretAPI*.dll
retention-days: 7
106 changes: 106 additions & 0 deletions SecretAPI.CodeGeneration/CallOnLoadGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
namespace SecretAPI.CodeGeneration;

/// <summary>
/// Code generator for CallOnLoad/CallOnUnload
/// </summary>
[Generator]
public class CallOnLoadGenerator : IIncrementalGenerator
{
private const string PluginBaseClassName = "Plugin";
private const string CallOnLoadAttributeLocation = "SecretAPI.Attribute.CallOnLoadAttribute";
private const string CallOnUnloadAttributeLocation = "SecretAPI.Attribute.CallOnUnloadAttribute";

/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<IMethodSymbol> methodProvider =
context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
static (ctx, _) =>
ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as IMethodSymbol)
.Where(static m => m is not null)!;

IncrementalValuesProvider<(IMethodSymbol method, bool isLoad, bool isUnload)> callProvider =
methodProvider.Select(static (method, _) => (
method,
HasAttribute(method, CallOnLoadAttributeLocation),
HasAttribute(method, CallOnUnloadAttributeLocation)))
.Where(static m => m.Item2 || m.Item3);

IncrementalValuesProvider<INamedTypeSymbol> pluginClassProvider =
context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax,
static (ctx, _) =>
ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol)
.Where(static c =>
c is { IsAbstract: false, BaseType.Name: PluginBaseClassName })!;

context.RegisterSourceOutput(pluginClassProvider.Combine(callProvider.Collect()), static (context, data) =>
{
Generate(context, data.Left, data.Right);
});
}

private static bool HasAttribute(IMethodSymbol? method, string attributeLocation)
{
if (method == null)
return false;

foreach (AttributeData attribute in method.GetAttributes())
{
if (attribute.AttributeClass?.ToDisplayString() == attributeLocation)
return true;
}

return false;
}

private static int GetPriority(IMethodSymbol method, string attributeLocation)
{
AttributeData? attribute = method.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == attributeLocation);
if (attribute == null)
return 0;

if (attribute.ConstructorArguments.Length > 0)
return (int)attribute.ConstructorArguments[0].Value!;

return 0;
}

private static void Generate(
SourceProductionContext context,
INamedTypeSymbol? pluginClassSymbol,
ImmutableArray<(IMethodSymbol method, bool isLoad, bool isUnload)> methods)
{
if (pluginClassSymbol == null || methods.IsEmpty)
return;

IMethodSymbol[] loadCalls = methods
.Where(m => m.isLoad)
.Select(m => m.method)
.OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation))
.ToArray();

IMethodSymbol[] unloadCalls = methods
.Where(m => m.isUnload)
.Select(m => m.method)
.OrderBy(m => GetPriority(m, CallOnUnloadAttributeLocation))
.ToArray();

CompilationUnitSyntax compilation = ClassBuilder.CreateBuilder(pluginClassSymbol)
.AddUsingStatements("System")
.AddModifiers(SyntaxKind.PartialKeyword)
.StartMethodCreation("OnLoad", "void")
.AddModifiers(SyntaxKind.PublicKeyword)
.AddStatements(MethodCallStatements(loadCalls))
.FinishMethodBuild()
.StartMethodCreation("OnUnload", "void")
.AddModifiers(SyntaxKind.PublicKeyword)
.AddStatements(MethodCallStatements(unloadCalls))
.FinishMethodBuild()
.Build();

context.AddSource($"{pluginClassSymbol.Name}.g.cs", compilation.ToFullString());
}
}
64 changes: 64 additions & 0 deletions SecretAPI.CodeGeneration/CodeBuilders/ClassBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace SecretAPI.CodeGeneration.CodeBuilders;

internal class ClassBuilder
{
private NamespaceDeclarationSyntax _namespaceDeclaration;
private ClassDeclarationSyntax _classDeclaration;
private string _className;

private readonly List<SyntaxToken> _modifiers = new();
private readonly List<UsingDirectiveSyntax> _usings = new();
private readonly List<MethodDeclarationSyntax> _methods = new();

private ClassBuilder(string @namespace, string className)
{
_namespaceDeclaration = NamespaceDeclaration(ParseName(@namespace));
_className = className;
_classDeclaration = ClassDeclaration(className);

AddUsingStatements("System.CodeDom.Compiler");
}

internal static ClassBuilder CreateBuilder(string @namespace, string className)
=> new(@namespace, className);

internal static ClassBuilder CreateBuilder(INamedTypeSymbol namedClass)
=> new(namedClass.ContainingNamespace.ToDisplayString(), namedClass.Name);

internal ClassBuilder AddUsingStatements(params string[] usingStatement)
{
foreach (string statement in usingStatement)
_usings.Add(UsingDirective(ParseName(statement)));

return this;
}

internal MethodBuilder StartMethodCreation(string methodName, string returnType) => new(this, methodName, returnType);

internal void AddMethodDefinition(MethodDeclarationSyntax method) => _methods.Add(method);

internal ClassBuilder AddModifiers(params SyntaxKind[] modifiers)
{
foreach (SyntaxKind token in modifiers)
_modifiers.Add(Token(token));

return this;
}

internal CompilationUnitSyntax Build()
{
_classDeclaration = _classDeclaration
.AddAttributeLists(GetGeneratedCodeAttributeListSyntax())
.AddModifiers(_modifiers.ToArray())
.AddMembers(_methods.Cast<MemberDeclarationSyntax>().ToArray());

_namespaceDeclaration = _namespaceDeclaration
.AddUsings(_usings.ToArray())
.AddMembers(_classDeclaration);

return CompilationUnit()
.AddMembers(_namespaceDeclaration)
.NormalizeWhitespace()
.WithLeadingTrivia(Comment("// <auto-generated>"), LineFeed, Comment("#pragma warning disable"), LineFeed, LineFeed);
}
}
53 changes: 53 additions & 0 deletions SecretAPI.CodeGeneration/CodeBuilders/MethodBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace SecretAPI.CodeGeneration.CodeBuilders;

internal class MethodBuilder
{
private readonly ClassBuilder _classBuilder;
private readonly List<SyntaxToken> _modifiers = new();
private readonly List<ParameterSyntax> _parameters = new();
private readonly List<StatementSyntax> _statements = new();
private readonly string _methodName;
private readonly string _returnType;

internal MethodBuilder(ClassBuilder classBuilder, string methodName, string returnType)
{
_classBuilder = classBuilder;
_methodName = methodName;
_returnType = returnType;
}

internal MethodBuilder AddStatements(params StatementSyntax[] statements)
{
_statements.AddRange(statements);
return this;
}

internal MethodBuilder AddParameters(params MethodParameter[] parameters)
{
foreach (MethodParameter parameter in parameters)
_parameters.Add(parameter.Syntax);

return this;
}

internal MethodBuilder AddModifiers(params SyntaxKind[] modifiers)
{
foreach (SyntaxKind token in modifiers)
_modifiers.Add(Token(token));

return this;
}

internal ClassBuilder FinishMethodBuild()
{
BlockSyntax body = _statements.Any() ? Block(_statements) : Block();

MethodDeclarationSyntax methodDeclaration = MethodDeclaration(ParseTypeName(_returnType), _methodName)
.AddModifiers(_modifiers.ToArray())
.AddParameterListParameters(_parameters.ToArray())
.WithBody(body);

_classBuilder.AddMethodDefinition(methodDeclaration);
return _classBuilder;
}
}
103 changes: 103 additions & 0 deletions SecretAPI.CodeGeneration/CustomCommandGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
namespace SecretAPI.CodeGeneration;

/// <summary>
/// Code generator for custom commands, creating validation etc.
/// </summary>
[Generator]
public class CustomCommandGenerator : IIncrementalGenerator
{
private const string CommandName = "CustomCommand";
private const string ExecuteMethodName = "Execute";
private const string ExecuteCommandMethodAttributeLocation = "SecretAPI.Features.Commands.Attributes.ExecuteCommandAttribute";

private static readonly MethodParameter ArgumentsParam =
new(
identifier: "arguments",
type: GetSingleGenericTypeSyntax("ArraySegment", SyntaxKind.StringKeyword)
);

private static readonly MethodParameter SenderParam =
new(
identifier: "sender",
type: IdentifierName("ICommandSender")
);

private static readonly MethodParameter ResponseParam =
new(
identifier: "response",
type: GetPredefinedTypeSyntax(SyntaxKind.StringKeyword),
modifiers: TokenList(
Token(SyntaxKind.OutKeyword))
);

/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<(INamedTypeSymbol?, ImmutableArray<MethodDeclarationSyntax>)> classProvider
= context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => node is ClassDeclarationSyntax,
static (ctx, cancel) =>
{
ClassDeclarationSyntax classSyntax = (ClassDeclarationSyntax)ctx.Node;
INamedTypeSymbol? typeSymbol = ModelExtensions.GetDeclaredSymbol(ctx.SemanticModel, classSyntax, cancel) as INamedTypeSymbol;
return (typeSymbol, GetExecuteMethods(ctx, classSyntax));
}).Where(tuple => tuple is { typeSymbol: not null, Item2.IsEmpty: false });

context.RegisterSourceOutput(classProvider, (ctx, tuple) => Generate(ctx, tuple.Item1!, tuple.Item2));
}

private static ImmutableArray<MethodDeclarationSyntax> GetExecuteMethods(
GeneratorSyntaxContext context,
ClassDeclarationSyntax classDeclarationSyntax)
{
List<MethodDeclarationSyntax> methods = new();
foreach (MethodDeclarationSyntax method in classDeclarationSyntax.Members.OfType<MethodDeclarationSyntax>())
{
if (!IsExecuteMethod(context, method))
continue;

methods.Add(method);
}

return methods.ToImmutableArray();
}

private static bool IsExecuteMethod(GeneratorSyntaxContext context, MethodDeclarationSyntax methodDeclarationSyntax)
{
foreach (AttributeListSyntax attributeListSyntax in methodDeclarationSyntax.AttributeLists)
{
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
{
ITypeSymbol? attributeTypeSymbol = ModelExtensions.GetTypeInfo(context.SemanticModel, attributeSyntax).Type;
if (attributeTypeSymbol != null && attributeTypeSymbol.ToDisplayString() == ExecuteCommandMethodAttributeLocation)
return true;
}
}

return false;
}

private static void Generate(
SourceProductionContext ctx,
INamedTypeSymbol namedClassSymbol,
ImmutableArray<MethodDeclarationSyntax> executeMethods)
{
if (namedClassSymbol.IsAbstract)
return;

if (namedClassSymbol.BaseType?.Name != CommandName)
return;

CompilationUnitSyntax compilation = ClassBuilder.CreateBuilder(namedClassSymbol)
.AddUsingStatements("System", "System.Collections.Generic")
.AddUsingStatements("CommandSystem")
.AddModifiers(SyntaxKind.PartialKeyword)
.StartMethodCreation(ExecuteMethodName, "bool")
.AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword)
.AddParameters(ArgumentsParam, SenderParam, ResponseParam)
.FinishMethodBuild()
.Build();

ctx.AddSource($"{namedClassSymbol.Name}.g.cs", compilation.ToFullString());
}
}
10 changes: 10 additions & 0 deletions SecretAPI.CodeGeneration/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
global using Microsoft.CodeAnalysis.CSharp.Syntax;
global using System.Collections.Immutable;
global using SecretAPI.CodeGeneration.CodeBuilders;
global using SecretAPI.CodeGeneration.Utils;

global using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
global using static Microsoft.CodeAnalysis.CSharp.SyntaxFacts;
global using static SecretAPI.CodeGeneration.Utils.Util;
Loading
Loading