diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..3d2453f --- /dev/null +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,6 @@ +### New Rules + + Rule ID | Category | Severity | Notes +------------|----------|----------|--------------------- + SG001 | Usage | Error | MustBeAccessibleMethod + SG002 | 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..7f14306 --- /dev/null +++ b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs @@ -0,0 +1,80 @@ +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"); + } + + 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().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("// "), LineFeed, 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..8361192 --- /dev/null +++ b/SecretAPI.SourceGenerators/Diagnostics.cs @@ -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); +} \ 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..dce991e --- /dev/null +++ b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs @@ -0,0 +1,130 @@ +namespace SecretAPI.SourceGenerators.Generators; + +/// +/// Code generator for CallOnLoad/CallOnUnload +/// TODO: Implement IRegister source generation +/// +[Generator] +public class CallOnLoadGenerator : IIncrementalGenerator +{ + private const string GeneratedClassName = "SecretApiGenerated"; + 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); + + 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"); + } +} \ 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..2a288bd 100644 --- a/SecretAPI/SecretApi.cs +++ b/SecretAPI/SecretApi.cs @@ -48,7 +48,7 @@ public class SecretApi : Plugin /// public override void Enable() { - CallOnLoadAttribute.Load(Assembly); + SecretApiGenerated.OnLoad(); } /// @@ -56,4 +56,4 @@ public override void Disable() { Harmony.UnpatchAll(Harmony.Id); } -} \ No newline at end of file +}