From e0137647f01f4b093053d7adc08300af28009e3c Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Mon, 27 Apr 2026 15:59:56 +0200
Subject: [PATCH 1/6] SourceGeneration
---
.../AnalyzerReleases.Shipped.md | 3 +
.../AnalyzerReleases.Unshipped.md | 7 +
.../Builders/Builder.cs | 19 +++
.../Builders/ClassBuilder.cs | 60 +++++++
.../Builders/MethodBuilder.cs | 44 +++++
SecretAPI.SourceGenerators/Diagnostics.cs | 28 ++++
.../Generators/CallOnLoadGenerator.cs | 152 ++++++++++++++++++
SecretAPI.SourceGenerators/GlobalUsings.cs | 19 +++
.../SecretAPI.SourceGenerators.csproj | 26 +++
.../Utils/GeneratedIdentifyUtils.cs | 19 +++
.../Utils/GenericTypeUtils.cs | 12 ++
.../Utils/MethodParameter.cs | 40 +++++
.../Utils/MethodUtils.cs | 20 +++
SecretAPI.SourceGenerators/Utils/TypeUtils.cs | 10 ++
SecretAPI.sln | 6 +
SecretAPI/SecretAPI.csproj | 2 +
SecretAPI/SecretApi.cs | 2 +-
17 files changed, 468 insertions(+), 1 deletion(-)
create mode 100644 SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md
create mode 100644 SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
create mode 100644 SecretAPI.SourceGenerators/Builders/Builder.cs
create mode 100644 SecretAPI.SourceGenerators/Builders/ClassBuilder.cs
create mode 100644 SecretAPI.SourceGenerators/Builders/MethodBuilder.cs
create mode 100644 SecretAPI.SourceGenerators/Diagnostics.cs
create mode 100644 SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
create mode 100644 SecretAPI.SourceGenerators/GlobalUsings.cs
create mode 100644 SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj
create mode 100644 SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
create mode 100644 SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs
create mode 100644 SecretAPI.SourceGenerators/Utils/MethodParameter.cs
create mode 100644 SecretAPI.SourceGenerators/Utils/MethodUtils.cs
create mode 100644 SecretAPI.SourceGenerators/Utils/TypeUtils.cs
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..ad9f9dc
--- /dev/null
+++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,7 @@
+### New Rules
+
+ Rule ID | Category | Severity | Notes
+------------|----------|----------|---------------------
+ SecretGen0 | Usage | Error | CA6000_AnalyzerName
+ SecretGen1 | Usage | Error | CA6000_AnalyzerName
+ SecretGen2 | Usage | Error | CA6000_AnalyzerName
\ 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..8b3e2b4
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Diagnostics.cs
@@ -0,0 +1,28 @@
+namespace SecretAPI.SourceGenerators;
+
+internal static class Diagnostics
+{
+ internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new(
+ "SecretGen0",
+ "Plugin class must be partial",
+ "Plugin class '{0}' is missing partial modifier",
+ "Usage",
+ DiagnosticSeverity.Error,
+ true);
+
+ internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new(
+ "SecretGen1",
+ "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(
+ "SecretGen2",
+ "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..7147eb0
--- /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, Tuple pluginInfo, 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, pluginInfo, m.method))
+ .Select(m => m.method)
+ .OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation))
+ .ToArray();
+
+ IMethodSymbol[] unloadCalls = methods
+ .Where(m => m.isUnload && ValidateMethod(context, pluginInfo, 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..eb69204
--- /dev/null
+++ b/SecretAPI.SourceGenerators/GlobalUsings.cs
@@ -0,0 +1,19 @@
+//? 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.GenericTypeUtils;
+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..063d3b1
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
@@ -0,0 +1,19 @@
+namespace SecretAPI.SourceGenerators.Utils;
+
+internal static class GeneratedIdentifyUtils
+{
+ private static AttributeSyntax GetGeneratedCodeAttributeSyntax()
+ => Attribute(IdentifierName("GeneratedCode"))
+ .WithArgumentList(
+ AttributeArgumentList(
+ SeparatedList(
+ new SyntaxNodeOrToken[]
+ {
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.CodeGeneration"))),
+ Token(SyntaxKind.CommaToken),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("1.0.0"))),
+ })));
+
+ internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax()
+ => AttributeList(SingletonSeparatedList(GetGeneratedCodeAttributeSyntax()));
+}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs b/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs
new file mode 100644
index 0000000..f530632
--- /dev/null
+++ b/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs
@@ -0,0 +1,12 @@
+namespace SecretAPI.SourceGenerators.Utils;
+
+internal static class GenericTypeUtils
+{
+ internal static TypeSyntax GetSingleGenericTypeSyntax(string genericName, SyntaxKind predefinedType)
+ => GenericName(genericName)
+ .WithTypeArgumentList(
+ TypeArgumentList(
+ SingletonSeparatedList(
+ PredefinedType(
+ Token(predefinedType)))));
+}
\ 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.sln b/SecretAPI.sln
index 4ed659a..ad97fde 100644
--- a/SecretAPI.sln
+++ b/SecretAPI.sln
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretAPI", "SecretAPI\Secr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretAPI.Examples", "SecretAPI.Examples\SecretAPI.Examples.csproj", "{0064C982-5FE1-4B65-82F9-2EEF85651188}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretAPI.SourceGenerators", "SecretAPI.SourceGenerators\SecretAPI.SourceGenerators.csproj", "{15C8D708-16DE-4533-AEB6-003C3EDCAD45}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{0064C982-5FE1-4B65-82F9-2EEF85651188}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0064C982-5FE1-4B65-82F9-2EEF85651188}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0064C982-5FE1-4B65-82F9-2EEF85651188}.Release|Any CPU.Build.0 = Release|Any CPU
+ {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
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..941f7b3 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";
From ff689db62795fd254cf33305e3409e957d4eec06 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Mon, 27 Apr 2026 16:01:24 +0200
Subject: [PATCH 2/6] Removed unused class
---
SecretAPI.SourceGenerators/GlobalUsings.cs | 1 -
.../Utils/GeneratedIdentifyUtils.cs | 2 +-
SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs | 12 ------------
3 files changed, 1 insertion(+), 14 deletions(-)
delete mode 100644 SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs
diff --git a/SecretAPI.SourceGenerators/GlobalUsings.cs b/SecretAPI.SourceGenerators/GlobalUsings.cs
index eb69204..8869bf3 100644
--- a/SecretAPI.SourceGenerators/GlobalUsings.cs
+++ b/SecretAPI.SourceGenerators/GlobalUsings.cs
@@ -13,7 +13,6 @@
global using SecretAPI.SourceGenerators.Utils;
//? Static utils from SecretAPI
-global using static SecretAPI.SourceGenerators.Utils.GenericTypeUtils;
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/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
index 063d3b1..da236f0 100644
--- a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
+++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
@@ -13,7 +13,7 @@ private static AttributeSyntax GetGeneratedCodeAttributeSyntax()
Token(SyntaxKind.CommaToken),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("1.0.0"))),
})));
-
+
internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax()
=> AttributeList(SingletonSeparatedList(GetGeneratedCodeAttributeSyntax()));
}
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs b/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs
deleted file mode 100644
index f530632..0000000
--- a/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace SecretAPI.SourceGenerators.Utils;
-
-internal static class GenericTypeUtils
-{
- internal static TypeSyntax GetSingleGenericTypeSyntax(string genericName, SyntaxKind predefinedType)
- => GenericName(genericName)
- .WithTypeArgumentList(
- TypeArgumentList(
- SingletonSeparatedList(
- PredefinedType(
- Token(predefinedType)))));
-}
\ No newline at end of file
From a9decf7a499b0b90b8c20fa932b5326e8a089e73 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Mon, 27 Apr 2026 16:18:00 +0200
Subject: [PATCH 3/6] Remove unused pluginInfo parameter
---
.../Generators/CallOnLoadGenerator.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
index 7147eb0..6171c97 100644
--- a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
+++ b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
@@ -69,7 +69,7 @@ private static int GetPriority(IMethodSymbol method, string attributeLocation)
return 0;
}
- private static bool ValidateMethod(SourceProductionContext context, Tuple pluginInfo, IMethodSymbol method)
+ private static bool ValidateMethod(SourceProductionContext context, IMethodSymbol method)
{
bool isValid = true;
@@ -119,13 +119,13 @@ private static void Generate(
}
IMethodSymbol[] loadCalls = methods
- .Where(m => m.isLoad && ValidateMethod(context, pluginInfo, m.method))
+ .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, pluginInfo, m.method))
+ .Where(m => m.isUnload && ValidateMethod(context, m.method))
.Select(m => m.method)
.OrderBy(m => GetPriority(m, CallOnUnloadAttributeLocation))
.ToArray();
From f6322bcc01477ec88c8a32d50b05abb4a5d00753 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Mon, 27 Apr 2026 16:26:55 +0200
Subject: [PATCH 4/6] Rules
---
SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md | 6 +++---
SecretAPI.SourceGenerators/Diagnostics.cs | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
index ad9f9dc..08f562d 100644
--- a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
+++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
@@ -2,6 +2,6 @@
Rule ID | Category | Severity | Notes
------------|----------|----------|---------------------
- SecretGen0 | Usage | Error | CA6000_AnalyzerName
- SecretGen1 | Usage | Error | CA6000_AnalyzerName
- SecretGen2 | Usage | Error | CA6000_AnalyzerName
\ No newline at end of file
+ SG001 | Usage | Error | MustBePartialPluginClass
+ SG002 | Usage | Error | MustBeAccessibleMethod
+ SG003 | Usage | Error | MustBeStaticMethod
\ No newline at end of file
diff --git a/SecretAPI.SourceGenerators/Diagnostics.cs b/SecretAPI.SourceGenerators/Diagnostics.cs
index 8b3e2b4..21d8073 100644
--- a/SecretAPI.SourceGenerators/Diagnostics.cs
+++ b/SecretAPI.SourceGenerators/Diagnostics.cs
@@ -3,7 +3,7 @@
internal static class Diagnostics
{
internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new(
- "SecretGen0",
+ "SG001",
"Plugin class must be partial",
"Plugin class '{0}' is missing partial modifier",
"Usage",
@@ -11,7 +11,7 @@ internal static class Diagnostics
true);
internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new(
- "SecretGen1",
+ "SG002",
"Method must be accessible",
"Method '{0}' has accessibility '{1}', which is not supported for generated calls",
"Usage",
@@ -19,7 +19,7 @@ internal static class Diagnostics
true);
internal static readonly DiagnosticDescriptor MustBeStaticMethod = new(
- "SecretGen2",
+ "SG003",
"Method must be static",
"Method '{0}' is not marked as static",
"Usage",
From de7fb2f34a504161ea0024ac3cd061dccac2de00 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Mon, 27 Apr 2026 16:37:47 +0200
Subject: [PATCH 5/6] Fix version number & OnLoad() not being utilized
---
SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs | 6 ++++--
SecretAPI/SecretApi.cs | 2 +-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
index da236f0..02a4988 100644
--- a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
+++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
@@ -2,6 +2,8 @@
internal static class GeneratedIdentifyUtils
{
+ private static SyntaxToken CurrentVersion => Literal(typeof(GeneratedIdentifyUtils).Assembly.GetName().Version.ToString());
+
private static AttributeSyntax GetGeneratedCodeAttributeSyntax()
=> Attribute(IdentifierName("GeneratedCode"))
.WithArgumentList(
@@ -9,9 +11,9 @@ private static AttributeSyntax GetGeneratedCodeAttributeSyntax()
SeparatedList(
new SyntaxNodeOrToken[]
{
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.CodeGeneration"))),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.SourceGenerators"))),
Token(SyntaxKind.CommaToken),
- AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("1.0.0"))),
+ AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, CurrentVersion)),
})));
internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax()
diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs
index 941f7b3..7c5931d 100644
--- a/SecretAPI/SecretApi.cs
+++ b/SecretAPI/SecretApi.cs
@@ -48,7 +48,7 @@ public partial class SecretApi : Plugin
///
public override void Enable()
{
- CallOnLoadAttribute.Load(Assembly);
+ OnLoad();
}
///
From 34cf80c9832fea052be2fbc41ba7f922c33d19e7 Mon Sep 17 00:00:00 2001
From: Eve <85962933+obvEve@users.noreply.github.com>
Date: Wed, 29 Apr 2026 21:54:33 +0200
Subject: [PATCH 6/6] Fix: Add SourceGenerators as a project to solution
---
SecretAPI.slnx | 1 +
1 file changed, 1 insertion(+)
diff --git a/SecretAPI.slnx b/SecretAPI.slnx
index 99c20fd..6995b49 100644
--- a/SecretAPI.slnx
+++ b/SecretAPI.slnx
@@ -1,4 +1,5 @@
+