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(); } ///