diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..60b59dd
--- /dev/null
+++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..08f562d
--- /dev/null
+++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,7 @@
+### New Rules
+
+ Rule ID | Category | Severity | Notes
+------------|----------|----------|---------------------
+ SG001 | Usage | Error | MustBePartialPluginClass
+ SG002 | Usage | Error | MustBeAccessibleMethod
+ SG003 | Usage | Error | MustBeStaticMethod
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Builders/Builder.cs b/SecretAPI.SourceGenerators/Builders/Builder.cs
new file mode 100644
index 0000000..4cc8b63
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Builders/Builder.cs
@@ -0,0 +1,19 @@
+namespace SecretAPI.SourceGenerators.Builders;
+
+///
+/// Base of a builder.
+///
+/// The this is handling.
+internal abstract class Builder
+ where TBuilder : Builder
+{
+ protected readonly List _modifiers = new();
+
+ internal TBuilder AddModifiers(params SyntaxKind[] modifiers)
+ {
+ foreach (SyntaxKind token in modifiers)
+ _modifiers.Add(Token(token));
+
+ return (TBuilder)this;
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs
new file mode 100644
index 0000000..671cf80
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs
@@ -0,0 +1,60 @@
+namespace SecretAPI.SourceGenerators.Builders;
+
+internal class ClassBuilder : Builder
+{
+ private NamespaceDeclarationSyntax _namespaceDeclaration;
+ private ClassDeclarationSyntax _classDeclaration;
+
+ private readonly List _usings = new();
+ private readonly List _methods = new();
+
+ private ClassBuilder(NamespaceDeclarationSyntax namespaceDeclaration, ClassDeclarationSyntax classDeclaration)
+ {
+ _namespaceDeclaration = namespaceDeclaration;
+ _classDeclaration = classDeclaration;
+
+ AddUsingStatements("System.CodeDom.Compiler");
+ }
+
+ internal static ClassBuilder CreateBuilder(INamedTypeSymbol namedClass)
+ => CreateBuilder(NamespaceDeclaration(ParseName(namedClass.ContainingNamespace.ToDisplayString())), ClassDeclaration(namedClass.Name));
+
+ internal static ClassBuilder CreateBuilder(NamespaceDeclarationSyntax namespaceDeclaration, ClassDeclarationSyntax classDeclaration)
+ => new(namespaceDeclaration, classDeclaration);
+
+ internal ClassBuilder AddUsingStatements(params string[] usingStatements)
+ {
+ foreach (string statement in usingStatements)
+ {
+ UsingDirectiveSyntax usings = UsingDirective(ParseName(statement));
+ if (!_usings.Any(existing => existing.IsEquivalentTo(usings)))
+ _usings.Add(usings);
+ }
+
+ return this;
+ }
+
+ internal MethodBuilder StartMethodCreation(string methodName, TypeSyntax returnType) => new(this, methodName, returnType);
+ internal MethodBuilder StartMethodCreation(string methodName, SyntaxKind returnType) => StartMethodCreation(methodName, GetPredefinedTypeSyntax(returnType));
+
+ internal void AddMethodDefinition(MethodDeclarationSyntax method) => _methods.Add(method);
+
+ internal CompilationUnitSyntax Build()
+ {
+ _classDeclaration = _classDeclaration
+ .AddAttributeLists(GetGeneratedCodeAttributeListSyntax())
+ .AddModifiers(_modifiers.ToArray())
+ .AddMembers(_methods.Cast().ToArray());
+
+ _namespaceDeclaration = _namespaceDeclaration
+ .AddUsings(_usings.ToArray())
+ .AddMembers(_classDeclaration);
+
+ return CompilationUnit()
+ .AddMembers(_namespaceDeclaration)
+ .NormalizeWhitespace()
+ .WithLeadingTrivia(Comment("// "), LineFeed, Comment("#pragma warning disable"), LineFeed, Comment("#nullable enable"), LineFeed, LineFeed);
+ }
+
+ internal void Build(SourceProductionContext context, string name) => context.AddSource(name, Build().ToFullString());
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Builders/MethodBuilder.cs b/SecretAPI.SourceGenerators/Builders/MethodBuilder.cs
new file mode 100644
index 0000000..6538e4a
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Builders/MethodBuilder.cs
@@ -0,0 +1,44 @@
+namespace SecretAPI.SourceGenerators.Builders;
+
+internal class MethodBuilder : Builder
+{
+ private readonly ClassBuilder _classBuilder;
+ private readonly List _parameters = new();
+ private readonly List _statements = new();
+ private readonly string _methodName;
+ private readonly TypeSyntax _returnType;
+
+ internal MethodBuilder(ClassBuilder classBuilder, string methodName, TypeSyntax 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 ClassBuilder FinishMethodBuild()
+ {
+ BlockSyntax body = _statements.Any() ? Block(_statements) : Block();
+
+ MethodDeclarationSyntax methodDeclaration = MethodDeclaration(_returnType, _methodName)
+ .AddModifiers(_modifiers.ToArray())
+ .AddParameterListParameters(_parameters.ToArray())
+ .WithBody(body);
+
+ _classBuilder.AddMethodDefinition(methodDeclaration);
+ return _classBuilder;
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Diagnostics.cs b/SecretAPI.SourceGenerators/Diagnostics.cs
new file mode 100644
index 0000000..21d8073
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Diagnostics.cs
@@ -0,0 +1,28 @@
+namespace SecretAPI.SourceGenerators;
+
+internal static class Diagnostics
+{
+ internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new(
+ "SG001",
+ "Plugin class must be partial",
+ "Plugin class '{0}' is missing partial modifier",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+
+ internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new(
+ "SG002",
+ "Method must be accessible",
+ "Method '{0}' has accessibility '{1}', which is not supported for generated calls",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+
+ internal static readonly DiagnosticDescriptor MustBeStaticMethod = new(
+ "SG003",
+ "Method must be static",
+ "Method '{0}' is not marked as static",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
new file mode 100644
index 0000000..6171c97
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
@@ -0,0 +1,152 @@
+namespace SecretAPI.SourceGenerators.Generators;
+
+///
+/// Code generator for CallOnLoad/CallOnUnload
+///
+[Generator]
+public class CallOnLoadGenerator : IIncrementalGenerator
+{
+ private const string PluginNamespace = "LabApi.Loader.Features.Plugins";
+ private const string PluginBaseClassName = "Plugin";
+ private const string CallOnLoadAttributeLocation = "SecretAPI.Attributes.CallOnLoadAttribute";
+ private const string CallOnUnloadAttributeLocation = "SecretAPI.Attributes.CallOnUnloadAttribute";
+
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ IncrementalValuesProvider 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<(ClassDeclarationSyntax?, INamedTypeSymbol?)> pluginClassProvider =
+ context.SyntaxProvider.CreateSyntaxProvider(
+ static (node, _) => node is ClassDeclarationSyntax,
+ static (ctx, _) => (
+ ctx.Node as ClassDeclarationSyntax, ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol))
+ .Where(static c => c.Item2 != null && !c.Item2.IsAbstract && c.Item2.BaseType?.Name == PluginBaseClassName &&
+ c.Item2.BaseType.ContainingNamespace.ToDisplayString() == PluginNamespace);
+
+ context.RegisterSourceOutput(pluginClassProvider.Combine(callProvider.Collect()), static (context, data) =>
+ {
+ Generate(context, new Tuple(data.Left.Item1, data.Left.Item2), 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 bool ValidateMethod(SourceProductionContext context, IMethodSymbol method)
+ {
+ bool isValid = true;
+
+ if (!method.IsStatic)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Diagnostics.MustBeStaticMethod,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+
+ isValid = false;
+ }
+
+ if (method.DeclaredAccessibility is Accessibility.Private)
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Diagnostics.MustBeAccessibleMethod,
+ method.Locations.FirstOrDefault(),
+ method.Name,
+ method.DeclaredAccessibility));
+
+ isValid = false;
+ }
+
+ return isValid;
+ }
+
+ private static void Generate(
+ SourceProductionContext context,
+ Tuple pluginInfo,
+ ImmutableArray<(IMethodSymbol method, bool isLoad, bool isUnload)> methods)
+ {
+ if (pluginInfo.Item1 == null || pluginInfo.Item2 == null || methods.IsEmpty)
+ return;
+
+ if (!pluginInfo.Item1.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
+ {
+ context.ReportDiagnostic(
+ Diagnostic.Create(
+ Diagnostics.MustBePartialPluginClass,
+ pluginInfo.Item1.GetLocation(),
+ pluginInfo.Item1.Identifier.Text
+ )
+ );
+ }
+
+ IMethodSymbol[] loadCalls = methods
+ .Where(m => m.isLoad && ValidateMethod(context, m.method))
+ .Select(m => m.method)
+ .OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation))
+ .ToArray();
+
+ IMethodSymbol[] unloadCalls = methods
+ .Where(m => m.isUnload && ValidateMethod(context, m.method))
+ .Select(m => m.method)
+ .OrderBy(m => GetPriority(m, CallOnUnloadAttributeLocation))
+ .ToArray();
+
+ if (!loadCalls.Any() && !unloadCalls.Any())
+ return;
+
+ ClassBuilder classBuilder = ClassBuilder.CreateBuilder(pluginInfo.Item2)
+ .AddUsingStatements("System")
+ .AddModifiers(SyntaxKind.PartialKeyword);
+
+ classBuilder.StartMethodCreation("OnLoad", SyntaxKind.VoidKeyword)
+ .AddModifiers(SyntaxKind.PublicKeyword)
+ .AddStatements(MethodCallStatements(loadCalls))
+ .FinishMethodBuild();
+
+ classBuilder.StartMethodCreation("OnUnload", SyntaxKind.VoidKeyword)
+ .AddModifiers(SyntaxKind.PublicKeyword)
+ .AddStatements(MethodCallStatements(unloadCalls))
+ .FinishMethodBuild();
+
+ classBuilder.Build(context, $"{pluginInfo.Item2.Name}.g.cs");
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/GlobalUsings.cs b/SecretAPI.SourceGenerators/GlobalUsings.cs
new file mode 100644
index 0000000..8869bf3
--- /dev/null
+++ b/SecretAPI.SourceGenerators/GlobalUsings.cs
@@ -0,0 +1,18 @@
+//? Utils from other places
+global using Microsoft.CodeAnalysis;
+global using Microsoft.CodeAnalysis.CSharp;
+global using Microsoft.CodeAnalysis.CSharp.Syntax;
+global using System.Collections.Immutable;
+
+//? Static utils from other places
+global using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+global using static Microsoft.CodeAnalysis.CSharp.SyntaxFacts;
+
+//? Utils from SecretAPI
+global using SecretAPI.SourceGenerators.Builders;
+global using SecretAPI.SourceGenerators.Utils;
+
+//? Static utils from SecretAPI
+global using static SecretAPI.SourceGenerators.Utils.GeneratedIdentifyUtils;
+global using static SecretAPI.SourceGenerators.Utils.MethodUtils;
+global using static SecretAPI.SourceGenerators.Utils.TypeUtils;
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj b/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj
new file mode 100644
index 0000000..b6bd4b5
--- /dev/null
+++ b/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj
@@ -0,0 +1,26 @@
+
+
+
+ netstandard2.0
+ 14
+ true
+ enable
+
+
+
+ true
+ false
+ Library
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
new file mode 100644
index 0000000..02a4988
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
@@ -0,0 +1,21 @@
+namespace SecretAPI.SourceGenerators.Utils;
+
+internal static class GeneratedIdentifyUtils
+{
+ private static SyntaxToken CurrentVersion => Literal(typeof(GeneratedIdentifyUtils).Assembly.GetName().Version.ToString());
+
+ private static AttributeSyntax GetGeneratedCodeAttributeSyntax()
+ => Attribute(IdentifierName("GeneratedCode"))
+ .WithArgumentList(
+ AttributeArgumentList(
+ SeparatedList(
+ new SyntaxNodeOrToken[]
+ {
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.SourceGenerators"))),
+ Token(SyntaxKind.CommaToken),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, CurrentVersion)),
+ })));
+
+ internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax()
+ => AttributeList(SingletonSeparatedList(GetGeneratedCodeAttributeSyntax()));
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Utils/MethodParameter.cs b/SecretAPI.SourceGenerators/Utils/MethodParameter.cs
new file mode 100644
index 0000000..b2dd8db
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Utils/MethodParameter.cs
@@ -0,0 +1,40 @@
+namespace SecretAPI.SourceGenerators.Utils;
+
+///
+/// Represents a method parameter used during code generation.
+///
+internal readonly struct MethodParameter
+{
+ private readonly SyntaxList _attributeLists;
+ private readonly SyntaxTokenList _modifiers;
+ private readonly TypeSyntax? _type;
+ private readonly SyntaxToken _identifier;
+ private readonly EqualsValueClauseSyntax? _default;
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// The name of the parameter.
+ /// The parameter type. May be for implicitly-typed parameters.
+ /// Optional parameter modifiers (e.g. ref, out, in).
+ /// Optional attribute lists applied to the parameter.
+ /// Optional default value.
+ internal MethodParameter(
+ string identifier,
+ TypeSyntax? type = null,
+ SyntaxTokenList modifiers = default,
+ SyntaxList attributeLists = default,
+ EqualsValueClauseSyntax? @default = null)
+ {
+ _identifier = IsValidIdentifier(identifier)
+ ? Identifier(identifier)
+ : throw new ArgumentException("Identifier is not valid.", nameof(identifier));
+
+ _type = type;
+ _modifiers = modifiers;
+ _attributeLists = attributeLists;
+ _default = @default;
+ }
+
+ public ParameterSyntax Syntax => Parameter(_attributeLists, _modifiers, _type, _identifier, _default);
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Utils/MethodUtils.cs b/SecretAPI.SourceGenerators/Utils/MethodUtils.cs
new file mode 100644
index 0000000..d0f4b05
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Utils/MethodUtils.cs
@@ -0,0 +1,20 @@
+namespace SecretAPI.SourceGenerators.Utils;
+
+internal static class MethodUtils
+{
+ internal static StatementSyntax MethodCallStatement(string typeName, string methodName) =>
+ MethodCallStatement(ParseTypeName(typeName), IdentifierName(methodName));
+
+ internal static StatementSyntax MethodCallStatement(TypeSyntax type, IdentifierNameSyntax method)
+ => ExpressionStatement(
+ InvocationExpression(
+ MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ type, method)));
+
+ internal static StatementSyntax[] MethodCallStatements(IMethodSymbol[] methodCalls)
+ {
+ IEnumerable statements = methodCalls.Select(s => MethodCallStatement(s.ContainingType.ToDisplayString(), s.Name));
+ return statements.ToArray();
+ }
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Utils/TypeUtils.cs b/SecretAPI.SourceGenerators/Utils/TypeUtils.cs
new file mode 100644
index 0000000..1b59b10
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Utils/TypeUtils.cs
@@ -0,0 +1,10 @@
+namespace SecretAPI.SourceGenerators.Utils;
+
+internal static class TypeUtils
+{
+ internal static PredefinedTypeSyntax GetPredefinedTypeSyntax(SyntaxKind kind)
+ => PredefinedType(Token(kind));
+
+ internal static TypeSyntax GetTypeSyntax(string typeIdentifier)
+ => IdentifierName(typeIdentifier);
+}
\ No newline at end of file
diff --git a/SecretAPI.slnx b/SecretAPI.slnx
index 99c20fd..6995b49 100644
--- a/SecretAPI.slnx
+++ b/SecretAPI.slnx
@@ -1,4 +1,5 @@
+
diff --git a/SecretAPI/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj
index 0302f70..055b9de 100644
--- a/SecretAPI/SecretAPI.csproj
+++ b/SecretAPI/SecretAPI.csproj
@@ -31,6 +31,8 @@
+
+
diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs
index 9033e9a..7c5931d 100644
--- a/SecretAPI/SecretApi.cs
+++ b/SecretAPI/SecretApi.cs
@@ -11,7 +11,7 @@
///
/// Main class handling loading API.
///
-public class SecretApi : Plugin
+public partial class SecretApi : Plugin
{
///
public override string Name => "SecretAPI";
@@ -48,7 +48,7 @@ public class SecretApi : Plugin
///
public override void Enable()
{
- CallOnLoadAttribute.Load(Assembly);
+ OnLoad();
}
///