Skip to content
Open
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: 3 additions & 0 deletions SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

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

Rule ID | Category | Severity | Notes
------------|----------|----------|---------------------
SG001 | Usage | Error | MustBePartialPluginClass
SG002 | Usage | Error | MustBeAccessibleMethod
SG003 | 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;
}
}
60 changes: 60 additions & 0 deletions SecretAPI.SourceGenerators/Builders/ClassBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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");
}

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<MemberDeclarationSyntax>().ToArray());

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

return CompilationUnit()
.AddMembers(_namespaceDeclaration)
.NormalizeWhitespace()
.WithLeadingTrivia(Comment("// <auto-generated>"), 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;
}
}
28 changes: 28 additions & 0 deletions SecretAPI.SourceGenerators/Diagnostics.cs
Original file line number Diff line number Diff line change
@@ -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);
}
152 changes: 152 additions & 0 deletions SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
namespace SecretAPI.SourceGenerators.Generators;

/// <summary>
/// Code generator for CallOnLoad/CallOnUnload
/// </summary>
[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";

/// <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<(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<ClassDeclarationSyntax?, INamedTypeSymbol?>(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<ClassDeclarationSyntax?, INamedTypeSymbol?> 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");
}
}
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;
26 changes: 26 additions & 0 deletions SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<PropertyGroup>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IncludeBuildOutput>false</IncludeBuildOutput>
<OutputItemType>Library</OutputItemType>
<IsPackable>true</IsPackable>
</PropertyGroup>

<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>
21 changes: 21 additions & 0 deletions SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs
Original file line number Diff line number Diff line change
@@ -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<AttributeArgumentSyntax>(
new SyntaxNodeOrToken[]
{
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.SourceGenerators"))),
Token(SyntaxKind.CommaToken),
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, CurrentVersion)),
})));

internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax()
=> AttributeList(SingletonSeparatedList(GetGeneratedCodeAttributeSyntax()));
}
Loading
Loading