Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
4 changes: 4 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@
# Examples
You can find examples inside the `SecretAPI.Examples` folder above, this contains some example settings, patches using categories and some more.

# Source Generation
- SecretAPI includes a SourceGenerator via the Nuget package ``SecretAPI.SourceGenerators``
- This will generate ``SecretApiGenerated.cs`` which can be used with ``CallOn(Un)LoadAttribute``

# Support
* For any issues create an [Issue](https://github.com/Misfiy/SecretAPI/issues/new) or contact me on [Discord](https://discord.gg/RYzahv3vfC).
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<Version>3.1.2</Version>
<Version>3.2.0</Version>
</PropertyGroup>

<PropertyGroup Label="Nuget">
Expand Down
1 change: 1 addition & 0 deletions SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

6 changes: 6 additions & 0 deletions SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### New Rules

Rule ID | Category | Severity | Notes
------------|----------|----------|---------------------
SG001 | Usage | Error | MustBeAccessibleMethod
SG002 | Usage | Error | MustBeStaticMethod
19 changes: 19 additions & 0 deletions SecretAPI.SourceGenerators/Builders/Builder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace SecretAPI.SourceGenerators.Builders;

/// <summary>
/// Base of a builder.
/// </summary>
/// <typeparam name="TBuilder">The <see cref="Builder{TBuilder}"/> this is handling.</typeparam>
internal abstract class Builder<TBuilder>
where TBuilder : Builder<TBuilder>
{
protected readonly List<SyntaxToken> _modifiers = new();

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

return (TBuilder)this;
}
}
80 changes: 80 additions & 0 deletions SecretAPI.SourceGenerators/Builders/ClassBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace SecretAPI.SourceGenerators.Builders;

internal class ClassBuilder : Builder<ClassBuilder>
{
private NamespaceDeclarationSyntax? _namespaceDeclaration;
private ClassDeclarationSyntax _classDeclaration;

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

private ClassBuilder(NamespaceDeclarationSyntax? namespaceDeclaration, ClassDeclarationSyntax classDeclaration)
{
_namespaceDeclaration = namespaceDeclaration;
_classDeclaration = classDeclaration;

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

private ClassBuilder(ClassDeclarationSyntax classDeclaration)
: this(null, classDeclaration)
{
}

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 static ClassBuilder CreateBuilder(ClassDeclarationSyntax classDeclaration) => new(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<MemberDeclarationSyntax>().ToArray());

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

CompilationUnitSyntax unit = CompilationUnit();

if (_namespaceDeclaration != null)
{
_namespaceDeclaration = _namespaceDeclaration
.AddUsings(_usings.ToArray())
.AddMembers(_classDeclaration);
unit = unit.AddMembers(_namespaceDeclaration);
}
else
{
unit = unit.AddUsings(_usings.ToArray()).AddMembers(_classDeclaration);
}

return unit
.NormalizeWhitespace()
.WithLeadingTrivia(Comment("// <auto-generated>"), LineFeed, LineFeed, Comment("#pragma warning disable"), LineFeed, Comment("#nullable enable"), LineFeed, LineFeed);
}

internal void Build(SourceProductionContext context, string name) => context.AddSource(name, Build().ToFullString());
}
44 changes: 44 additions & 0 deletions SecretAPI.SourceGenerators/Builders/MethodBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace SecretAPI.SourceGenerators.Builders;

internal class MethodBuilder : Builder<MethodBuilder>
{
private readonly ClassBuilder _classBuilder;
private readonly List<ParameterSyntax> _parameters = new();
private readonly List<StatementSyntax> _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;
}
}
20 changes: 20 additions & 0 deletions SecretAPI.SourceGenerators/Diagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace SecretAPI.SourceGenerators;

internal static class Diagnostics
{
internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new(
"SG001",
"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(
"SG002",
"Method must be static",
"Method '{0}' is not marked as static",
"Usage",
DiagnosticSeverity.Error,
true);
}
130 changes: 130 additions & 0 deletions SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
namespace SecretAPI.SourceGenerators.Generators;

/// <summary>
/// Code generator for CallOnLoad/CallOnUnload
/// TODO: Implement IRegister source generation
/// </summary>
[Generator]
public class CallOnLoadGenerator : IIncrementalGenerator
{
private const string GeneratedClassName = "SecretApiGenerated";
private const string CallOnLoadAttributeLocation = "SecretAPI.Attributes.CallOnLoadAttribute";
private const string CallOnUnloadAttributeLocation = "SecretAPI.Attributes.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);

context.RegisterSourceOutput(callProvider.Collect(), Generate);
}

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,
ImmutableArray<(IMethodSymbol method, bool isLoad, bool isUnload)> methods)
{
if (methods.IsEmpty)
return;

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)
ClassBuilder classBuilder = ClassBuilder.CreateBuilder(ClassDeclaration(GeneratedClassName))
.AddUsingStatements("System")
.AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword);

classBuilder.StartMethodCreation("OnLoad", SyntaxKind.VoidKeyword)
.AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword)
.AddStatements(MethodCallStatements(loadCalls))
.FinishMethodBuild();

classBuilder.StartMethodCreation("OnUnload", SyntaxKind.VoidKeyword)
.AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword)
.AddStatements(MethodCallStatements(unloadCalls))
.FinishMethodBuild();

classBuilder.Build(context, $"{GeneratedClassName}.g.cs");
}
}
18 changes: 18 additions & 0 deletions SecretAPI.SourceGenerators/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 39 additions & 0 deletions SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>14</LangVersion>
<ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup Label="Nuget">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Title>SecretAPI.SourceGenerators</Title>
<Description>Source Generators for SecretAPI.</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>

<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>

<ItemGroup>
<None Include="..\.github\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
</Project>
Loading
Loading