From 7385f20d4c444b46b2311d8121dc4489feee2bee Mon Sep 17 00:00:00 2001 From: Hamunii Date: Mon, 7 Apr 2025 16:30:27 +0300 Subject: [PATCH 01/23] strip version metadata for BepInEx 5 --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 30 +++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 395f05c..14af190 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -59,6 +59,22 @@ public void Initialize(GeneratorInitializationContext context) return (string?)attribute?.ConstructorArguments.Single().Value; } + private static bool IsPreferStripInformationalVersionInfo(GeneratorExecutionContext context) + { + var bepInExAssemblyReference = + context.Compilation.SourceModule.ReferencedAssemblySymbols.FirstOrDefault(x => + x.Identity.Name == "BepInEx" + ); + + // BepInEx 5 doesn't support plugins having version pre-release or build metadata + // such as 1.0.0-beta or 1.0.0+97ec53d20958b88581680d4d3c15ba59a8900ed5 + bool isBepInEx5 = + bepInExAssemblyReference is not null + && bepInExAssemblyReference.Identity.Version.Major == 5; + + return isBepInEx5; + } + private enum AutoType { Plugin, @@ -110,7 +126,17 @@ public void Execute(GeneratorExecutionContext context) var arguments = attribute.ConstructorArguments.Select(x => x.IsNull ? null : (string)x.Value!).ToArray(); var id = arguments[0] ?? context.Compilation.AssemblyName; var name = arguments[1] ?? GetAssemblyAttribute(context, nameof(AssemblyTitleAttribute)) ?? context.Compilation.AssemblyName; - var version = arguments[2] ?? GetAssemblyAttribute(context, nameof(AssemblyInformationalVersionAttribute)) ?? GetAssemblyAttribute(context, nameof(AssemblyVersionAttribute)); + var version = arguments[2]; + if (version is null) + { + var informationalVersion = + GetAssemblyAttribute(context, nameof(AssemblyInformationalVersionAttribute)); + + if (informationalVersion is null) + version = GetAssemblyAttribute(context, nameof(AssemblyVersionAttribute)); + else if (IsPreferStripInformationalVersionInfo(context)) + version = informationalVersion.Split('-', '+')[0]; + } var attributeName = type switch { From 437436fb083c3550ed1e046d7e7f717ecef814bf Mon Sep 17 00:00:00 2001 From: Hamunii Date: Mon, 7 Apr 2025 16:35:02 +0300 Subject: [PATCH 02/23] fix not setting version if not BepInEx 5 --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 14af190..f79b7db 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -136,6 +136,8 @@ public void Execute(GeneratorExecutionContext context) version = GetAssemblyAttribute(context, nameof(AssemblyVersionAttribute)); else if (IsPreferStripInformationalVersionInfo(context)) version = informationalVersion.Split('-', '+')[0]; + else + version = informationalVersion; } var attributeName = type switch From 94387830864c3157f402948955d30be91f7a2bcc Mon Sep 17 00:00:00 2001 From: Hamunii Date: Wed, 23 Apr 2025 14:43:22 +0300 Subject: [PATCH 03/23] make it incremental --- .gitignore | 1 + BepInEx.AutoPlugin.sln | 10 + BepInEx.AutoPlugin/AutoPluginGenerator.cs | 353 ++++++++++--------- BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj | 24 +- BepInEx.AutoPlugin/BepInEx.AutoPlugin.props | 8 + Directory.Build.props | 20 ++ NuGet.Config | 6 + README.md | 14 +- TestPlugin/TestPlugin.cs | 20 ++ TestPlugin/TestPlugin.csproj | 24 ++ 10 files changed, 316 insertions(+), 164 deletions(-) create mode 100644 BepInEx.AutoPlugin/BepInEx.AutoPlugin.props create mode 100644 Directory.Build.props create mode 100644 NuGet.Config create mode 100644 TestPlugin/TestPlugin.cs create mode 100644 TestPlugin/TestPlugin.csproj diff --git a/.gitignore b/.gitignore index 14a4d8f..a7c9ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +artifacts/ bin/ obj/ /packages/ diff --git a/BepInEx.AutoPlugin.sln b/BepInEx.AutoPlugin.sln index 2d66936..6629a6f 100644 --- a/BepInEx.AutoPlugin.sln +++ b/BepInEx.AutoPlugin.sln @@ -1,7 +1,10 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.AutoPlugin", "BepInEx.AutoPlugin\BepInEx.AutoPlugin.csproj", "{DF01D949-82F0-4CCA-A094-903C8F30E691}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "TestPlugin\TestPlugin.csproj", "{6C291C8C-CC69-45BE-9235-421F7B12B0B2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +15,12 @@ Global {DF01D949-82F0-4CCA-A094-903C8F30E691}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF01D949-82F0-4CCA-A094-903C8F30E691}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF01D949-82F0-4CCA-A094-903C8F30E691}.Release|Any CPU.Build.0 = Release|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index f79b7db..dbdf1f9 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -1,191 +1,230 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; +using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -namespace BepInEx.AutoPlugin -{ - [Generator] - public class AutoPluginGenerator : ISourceGenerator - { - private const string AttributeCode = @"// -namespace BepInEx -{ - [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.Conditional(""CodeGeneration"")] - internal sealed class BepInAutoPluginAttribute : System.Attribute - { - public BepInAutoPluginAttribute(string id = null, string name = null, string version = null) {} - } -} +namespace BepInEx.AutoPlugin; -namespace BepInEx.Preloader.Core.Patching +[Generator] +public sealed class AutoPluginGenerator : IIncrementalGenerator { - [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.Conditional(""CodeGeneration"")] - internal sealed class PatcherAutoPluginAttribute : System.Attribute - { - public PatcherAutoPluginAttribute(string id = null, string name = null, string version = null) {} - } -} -"; - - private class SyntaxReceiver : ISyntaxContextReceiver + const string BepInAutoPluginAttr = "BepInEx.BepInAutoPluginAttribute"; + const string PatcherAutoPluginAttr = + "BepInEx.Preloader.Core.Patching.PatcherAutoPluginAttribute"; + const string BepInPluginAttr = "BepInEx.BepInPlugin"; + const string PatcherPluginInfoAttr = "BepInEx.Preloader.Core.Patching.PatcherPluginInfo"; + const string AttributeCode = """ + // + #nullable enable + namespace BepInEx { - public List CandidateTypes { get; } = new List(); - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [System.Diagnostics.Conditional("CodeGeneration")] + internal sealed class BepInAutoPluginAttribute : System.Attribute { - if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Any()) - { - CandidateTypes.Add(classDeclarationSyntax); - } + public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) {} } } - public void Initialize(GeneratorInitializationContext context) + namespace BepInEx.Preloader.Core.Patching { - context.RegisterForPostInitialization(i => i.AddSource("BepInAutoPluginAttribute", AttributeCode)); - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [System.Diagnostics.Conditional("CodeGeneration")] + internal sealed class PatcherAutoPluginAttribute : System.Attribute + { + public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) {} + } } + """; - private static string? GetAssemblyAttribute(GeneratorExecutionContext context, string name) + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => { - var attribute = context.Compilation.Assembly.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Name == name); - return (string?)attribute?.ConstructorArguments.Single().Value; - } + ctx.AddSource("BepInAutoPluginAttribute", AttributeCode); + }); - private static bool IsPreferStripInformationalVersionInfo(GeneratorExecutionContext context) - { - var bepInExAssemblyReference = - context.Compilation.SourceModule.ReferencedAssemblySymbols.FirstOrDefault(x => - x.Identity.Name == "BepInEx" - ); - - // BepInEx 5 doesn't support plugins having version pre-release or build metadata - // such as 1.0.0-beta or 1.0.0+97ec53d20958b88581680d4d3c15ba59a8900ed5 - bool isBepInEx5 = - bepInExAssemblyReference is not null - && bepInExAssemblyReference.Identity.Version.Major == 5; - - return isBepInEx5; - } + var references = context + .CompilationProvider.SelectMany((compilation, _) => compilation.ReferencedAssemblyNames) + .Collect(); + + IncrementalValueProvider isBepInEx5 = references.Select( + (refs, _) => + { + var bepInEx = refs.FirstOrDefault(r => r.Name.Equals("BepInEx")); + + // BepInEx 5 doesn't support plugins having version pre-release or build metadata + // such as 1.0.0-beta or 1.0.0+97ec53d20958b88581680d4d3c15ba59a8900ed5 + bool isBepInEx5 = bepInEx is not null && bepInEx.Version.Major == 5; - private enum AutoType + return isBepInEx5; + } + ); + + IncrementalValueProvider pluginProps = context + .AnalyzerConfigOptionsProvider.Combine(isBepInEx5) + .Select( + static (info, _) => + { + var (options, isBepInEx5) = info; + options.GlobalOptions.TryGetValue( + "build_property.AssemblyName", + out var assName + ); + options.GlobalOptions.TryGetValue( + "build_property.Product", + out var productName + ); + options.GlobalOptions.TryGetValue("build_property.Version", out var version); + options.GlobalOptions.TryGetValue( + "build_property.BepInAutoPluginStripBuildMetadata", + out var stripMetadata + ); + + // Afaik these should always be set. + assName ??= "unknown"; + productName ??= "unknown"; + version ??= "0.0.0.0"; + + bool isStripMetadata = + stripMetadata?.Equals("true", StringComparison.InvariantCultureIgnoreCase) + ?? false; + + if (isBepInEx5) + { + version = version.Split('-', '+')[0]; + } + else if (isStripMetadata) + { + version = version.Split('+')[0]; + } + + return new PluginProps(assName, productName, version); + } + ); + + IncrementalValuesProvider classesWithBepInAutoPlugin = context + .SyntaxProvider.ForAttributeWithMetadataName( + BepInAutoPluginAttr, + static (s, _) => s is ClassDeclarationSyntax, + static (ctx, _) => + ToPluginClassWithAttribute(ctx, BepInPluginAttr, BepInAutoPluginAttr) + ) + .Where(x => x is not null)!; + + context.RegisterSourceOutput(classesWithBepInAutoPlugin.Combine(pluginProps), WriteClass); + + IncrementalValuesProvider classesWithPatcherAutoPlugin = context + .SyntaxProvider.ForAttributeWithMetadataName( + PatcherAutoPluginAttr, + static (s, _) => s is ClassDeclarationSyntax, + static (ctx, _) => + ToPluginClassWithAttribute(ctx, PatcherPluginInfoAttr, PatcherAutoPluginAttr) + ) + .Where(x => x is not null)!; + + context.RegisterSourceOutput(classesWithPatcherAutoPlugin.Combine(pluginProps), WriteClass); + } + + static PluginClass? ToPluginClassWithAttribute( + GeneratorAttributeSyntaxContext ctx, + string attributeFqn, + string autoAttributeFqn + ) + { + var target = ctx.TargetNode; + if (ctx.SemanticModel.GetDeclaredSymbol(target) is not INamedTypeSymbol typeSymbol) { - Plugin, - Patcher, + return null; } - public void Execute(GeneratorExecutionContext context) + var attr = ctx.Attributes.FirstOrDefault(att => + att.AttributeClass?.ToDisplayString() == autoAttributeFqn + ); + + if (attr is null || attr.ConstructorArguments.Length != 3) + return null; + + var id = attr.ConstructorArguments[0].Value as string; + var name = attr.ConstructorArguments[1].Value as string; + var version = attr.ConstructorArguments[2].Value as string; + + var ns = typeSymbol.ContainingNamespace; + var containingNamespace = ns.IsGlobalNamespace ? null : ns.ToDisplayString(); + + return new PluginClass( + containingNamespace, + typeSymbol.Name, + attributeFqn, + new PluginProps(id, name, version) + ); + } + + private void WriteClass( + SourceProductionContext context, + (PluginClass plugin, PluginProps pluginProps) source + ) + { + try { - try - { - if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) - return; + var (plugin, props) = source; - var pluginAttributeType = context.Compilation.GetTypeByMetadataName("BepInEx.BepInAutoPluginAttribute") ?? throw new NullReferenceException("Couldn't find BepInAutoPluginAttribute"); - var patcherAttributeType = context.Compilation.GetTypeByMetadataName("BepInEx.Preloader.Core.Patching.PatcherAutoPluginAttribute") ?? throw new NullReferenceException("Couldn't find PatcherAutoPluginAttribute"); + StringBuilder sb = new(); - foreach (var classDeclarationSyntax in receiver.CandidateTypes) - { - var model = context.Compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); - var typeSymbol = (INamedTypeSymbol)model.GetDeclaredSymbol(classDeclarationSyntax)!; + sb.AppendLine("// "); - AttributeData? attribute = null; - AutoType? type = null; + string? id = plugin.Overrides.Id ?? props.Id; + string? name = plugin.Overrides.Name ?? props.Name; + string? version = plugin.Overrides.Version ?? props.Version; + string pluginClass = plugin.ClassName; - foreach (var attributeData in typeSymbol.GetAttributes()) - { - if (attributeData.AttributeClass == null) continue; - - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, pluginAttributeType)) - { - type = AutoType.Plugin; - attribute = attributeData; - break; - } - - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, patcherAttributeType)) - { - type = AutoType.Patcher; - attribute = attributeData; - break; - } - } + if (plugin.Namespace is not null) + { + sb.Append("namespace ").AppendLine(plugin.Namespace).AppendLine("{"); + } - if (attribute == null || type == null) + var classStr = $$""" + [{{plugin.BepInAttribute}}({{pluginClass}}.Id, "{{name}}", "{{version}}")] + partial class {{pluginClass}} { - continue; + /// + /// The Id of . + /// + public const string Id = "{{id}}"; + + /// + /// Gets the name of . + /// + public static string Name => "{{name}}"; + + /// + /// Gets the version of . + /// + public static string Version => "{{version}}"; } + """; - var arguments = attribute.ConstructorArguments.Select(x => x.IsNull ? null : (string)x.Value!).ToArray(); - var id = arguments[0] ?? context.Compilation.AssemblyName; - var name = arguments[1] ?? GetAssemblyAttribute(context, nameof(AssemblyTitleAttribute)) ?? context.Compilation.AssemblyName; - var version = arguments[2]; - if (version is null) - { - var informationalVersion = - GetAssemblyAttribute(context, nameof(AssemblyInformationalVersionAttribute)); - - if (informationalVersion is null) - version = GetAssemblyAttribute(context, nameof(AssemblyVersionAttribute)); - else if (IsPreferStripInformationalVersionInfo(context)) - version = informationalVersion.Split('-', '+')[0]; - else - version = informationalVersion; - } + sb.AppendLine(classStr); - var attributeName = type switch - { - AutoType.Plugin => "BepInEx.BepInPlugin", - AutoType.Patcher => "BepInEx.Preloader.Core.Patching.PatcherPluginInfo", - _ => throw new ArgumentOutOfRangeException(), - }; - - var source = SourceText.From($@"// -namespace {typeSymbol.ContainingNamespace.ToDisplayString()} -{{ - [{attributeName}({typeSymbol.Name}.Id, ""{name}"", ""{version}"")] - public partial class {typeSymbol.Name} - {{ - /// - /// Id of the . - /// - public const string Id = ""{id}""; - - /// - /// Gets the name of the . - /// - public static string Name => ""{name}""; - - /// - /// Gets the version of the . - /// - public static string Version => ""{version}""; - }} -}} -", Encoding.UTF8); - - context.AddSource($"{typeSymbol.Name}_Auto{type}.cs", source); - } - } - catch (Exception e) + if (plugin.Namespace is not null) { - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor( - "ERROR", - $"An exception was thrown by the {nameof(AutoPluginGenerator)}", - $"An exception was thrown by the {nameof(AutoPluginGenerator)} generator: {e.ToString().Replace("\n", ",")}", - nameof(AutoPluginGenerator), - DiagnosticSeverity.Error, - true), - Location.None)); + sb.AppendLine("}"); } + + var fileName = $"{plugin.Namespace}.{plugin.ClassName}.AutoPlugin.g.cs"; + context.AddSource(fileName, sb.ToString()); + } + catch (Exception ex) + { + throw new Exception(ex.ToString().Replace('\n', ',')); } } + + public sealed record PluginClass( + string? Namespace, + string ClassName, + string BepInAttribute, + PluginProps Overrides + ); + + public sealed record PluginProps(string? Id, string? Name, string? Version); } diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj index e544e4d..9c2efd7 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj @@ -2,9 +2,6 @@ netstandard2.0 - latest - enable - embedded 1.1.0 dev @@ -18,13 +15,28 @@ https://github.com/BepInEx/BepInEx.AutoPlugin LGPL-3.0-or-later Generates BepInPlugin attribute and static plugin info for you + + true - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props new file mode 100644 index 0000000..06b7716 --- /dev/null +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..6a5586c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,20 @@ + + + + latest + enable + enable + true + embedded + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./ + + + + true + + + diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..25c5bbd --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md index 10422db..2574f36 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![CI](https://github.com/BepInEx/BepInEx.AutoPlugin/workflows/CI/badge.svg)](https://github.com/BepInEx/BepInEx.AutoPlugin/actions) [![NuGet](https://img.shields.io/endpoint?color=blue&logo=NuGet&label=NuGet&url=https://shields.kzu.io/v/BepInEx.AutoPlugin?feed=nuget.bepinex.dev/v3/index.json)](https://nuget.bepinex.dev/packages/BepInEx.AutoPlugin) -Source generator that turns +Incremental C# Source generator that turns ```cs [BepInAutoPlugin("com.example.ExamplePlugin")] @@ -23,3 +23,15 @@ public class ExamplePlugin : BaseUnityPlugin public static string Version => "0.1.0"; } ``` + +## Configuration + +AutoPlugin allows stripping version build metadata to turn `1.0.0-beta+1234567890` into `1.0.0-beta` for the `Version` property by setting the following in your csproj: + +```xml + + true + +``` + +Note that this will do nothing on BepInEx 5 as it only accepts a version number without a pre-release version or build metadata, so it strips both. diff --git a/TestPlugin/TestPlugin.cs b/TestPlugin/TestPlugin.cs new file mode 100644 index 0000000..193d363 --- /dev/null +++ b/TestPlugin/TestPlugin.cs @@ -0,0 +1,20 @@ +using BepInEx; +using BepInEx.Preloader.Core.Patching; + +[BepInAutoPlugin] +public partial class PluginInGlobalNamespace { } + +namespace Plugin +{ + [BepInAutoPlugin(id: "my id", name: "my name", version: "my version")] + public partial class MyPluginWithOverrides { } + + // [PatcherAutoPlugin] + public partial class MyPatcherPlugin { } + + namespace Nested + { + [BepInAutoPlugin] + public partial class NestedPlugin { } + } +} diff --git a/TestPlugin/TestPlugin.csproj b/TestPlugin/TestPlugin.csproj new file mode 100644 index 0000000..096613a --- /dev/null +++ b/TestPlugin/TestPlugin.csproj @@ -0,0 +1,24 @@ + + + + test.TestPlugin + Test Plugin + 1.0.1-beta+69 + net9.0 + true + true + + + + + + + + + + + From a6c5eb1584c455a5073f2e6a1e43b58e836b8ff9 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Wed, 23 Apr 2025 15:26:15 +0300 Subject: [PATCH 04/23] add .g.cs to BepInAutoPluginAttribute source --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index dbdf1f9..e385aec 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -40,7 +40,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => { - ctx.AddSource("BepInAutoPluginAttribute", AttributeCode); + ctx.AddSource("BepInAutoPluginAttribute.g.cs", AttributeCode); }); var references = context From 517f349560c0a78951df6a2d4e92c80fb3985a0f Mon Sep 17 00:00:00 2001 From: Hamunii Date: Wed, 23 Apr 2025 15:51:29 +0300 Subject: [PATCH 05/23] target oldest roslyn version possible for maximum support --- BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj index 9c2efd7..e4cfdd9 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj @@ -20,8 +20,8 @@ - - + + all From 5d8e3dba9139137e5308691f00f7718bad7b153a Mon Sep 17 00:00:00 2001 From: Hamunii Date: Wed, 23 Apr 2025 16:01:41 +0300 Subject: [PATCH 06/23] remove probably stupid code --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index e385aec..88fbaca 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -43,9 +43,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ctx.AddSource("BepInAutoPluginAttribute.g.cs", AttributeCode); }); - var references = context - .CompilationProvider.SelectMany((compilation, _) => compilation.ReferencedAssemblyNames) - .Collect(); + var references = context.CompilationProvider.Select( + (compilation, _) => compilation.ReferencedAssemblyNames + ); IncrementalValueProvider isBepInEx5 = references.Select( (refs, _) => From 74bc4ad9cd6f5e74144e36231028002b0fa69ed3 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 10:47:54 +0300 Subject: [PATCH 07/23] resolve most issues --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 122 ++++++++++++---------- TestPlugin/TestPlugin.csproj | 2 +- 2 files changed, 67 insertions(+), 57 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 88fbaca..68f657b 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -1,5 +1,6 @@ using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace BepInEx.AutoPlugin; @@ -7,19 +8,19 @@ namespace BepInEx.AutoPlugin; [Generator] public sealed class AutoPluginGenerator : IIncrementalGenerator { - const string BepInAutoPluginAttr = "BepInEx.BepInAutoPluginAttribute"; - const string PatcherAutoPluginAttr = + const string BepInAutoPluginAttribute = "BepInEx.BepInAutoPluginAttribute"; + const string PatcherAutoPluginAttribute = "BepInEx.Preloader.Core.Patching.PatcherAutoPluginAttribute"; - const string BepInPluginAttr = "BepInEx.BepInPlugin"; - const string PatcherPluginInfoAttr = "BepInEx.Preloader.Core.Patching.PatcherPluginInfo"; + const string BepInPluginAttribute = "BepInEx.BepInPlugin"; + const string PatcherPluginInfoAttribute = "BepInEx.Preloader.Core.Patching.PatcherPluginInfo"; const string AttributeCode = """ // #nullable enable namespace BepInEx { - [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.Conditional("CodeGeneration")] - internal sealed class BepInAutoPluginAttribute : System.Attribute + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("CodeGeneration")] + internal sealed class BepInAutoPluginAttribute : global::System.Attribute { public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) {} } @@ -27,9 +28,9 @@ public BepInAutoPluginAttribute(string? id = null, string? name = null, string? namespace BepInEx.Preloader.Core.Patching { - [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.Conditional("CodeGeneration")] - internal sealed class PatcherAutoPluginAttribute : System.Attribute + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("CodeGeneration")] + internal sealed class PatcherAutoPluginAttribute : global::System.Attribute { public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) {} } @@ -68,7 +69,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var (options, isBepInEx5) = info; options.GlobalOptions.TryGetValue( "build_property.AssemblyName", - out var assName + out var assemblyName ); options.GlobalOptions.TryGetValue( "build_property.Product", @@ -81,7 +82,7 @@ out var stripMetadata ); // Afaik these should always be set. - assName ??= "unknown"; + assemblyName ??= "unknown"; productName ??= "unknown"; version ??= "0.0.0.0"; @@ -98,55 +99,65 @@ out var stripMetadata version = version.Split('+')[0]; } - return new PluginProps(assName, productName, version); + return new PluginProps(assemblyName, productName, version); } ); IncrementalValuesProvider classesWithBepInAutoPlugin = context .SyntaxProvider.ForAttributeWithMetadataName( - BepInAutoPluginAttr, - static (s, _) => s is ClassDeclarationSyntax, - static (ctx, _) => - ToPluginClassWithAttribute(ctx, BepInPluginAttr, BepInAutoPluginAttr) + BepInAutoPluginAttribute, + static (s, _) => IsValidPluginClass(s), + static (ctx, _) => ToPluginClass(ctx) ) - .Where(x => x is not null)!; + .Where(x => x is not null) + .Select((x, _) => (PluginClass)x!); - context.RegisterSourceOutput(classesWithBepInAutoPlugin.Combine(pluginProps), WriteClass); + context.RegisterSourceOutput( + classesWithBepInAutoPlugin.Combine(pluginProps), + static (context, source) => WriteClass(context, source, BepInPluginAttribute) + ); IncrementalValuesProvider classesWithPatcherAutoPlugin = context .SyntaxProvider.ForAttributeWithMetadataName( - PatcherAutoPluginAttr, - static (s, _) => s is ClassDeclarationSyntax, - static (ctx, _) => - ToPluginClassWithAttribute(ctx, PatcherPluginInfoAttr, PatcherAutoPluginAttr) + PatcherAutoPluginAttribute, + static (s, _) => IsValidPluginClass(s), + static (ctx, _) => ToPluginClass(ctx) ) - .Where(x => x is not null)!; + .Where(x => x is not null) + .Select((x, _) => (PluginClass)x!); - context.RegisterSourceOutput(classesWithPatcherAutoPlugin.Combine(pluginProps), WriteClass); + context.RegisterSourceOutput( + classesWithPatcherAutoPlugin.Combine(pluginProps), + static (context, source) => WriteClass(context, source, PatcherPluginInfoAttribute) + ); } - static PluginClass? ToPluginClassWithAttribute( - GeneratorAttributeSyntaxContext ctx, - string attributeFqn, - string autoAttributeFqn - ) + static bool IsValidPluginClass(SyntaxNode node) + { + if (node is not ClassDeclarationSyntax { Parent: not TypeDeclarationSyntax }) + { + return false; + } + + return true; + } + + static PluginClass? ToPluginClass(GeneratorAttributeSyntaxContext ctx) { - var target = ctx.TargetNode; + var target = (ClassDeclarationSyntax)ctx.TargetNode; if (ctx.SemanticModel.GetDeclaredSymbol(target) is not INamedTypeSymbol typeSymbol) { return null; } - var attr = ctx.Attributes.FirstOrDefault(att => - att.AttributeClass?.ToDisplayString() == autoAttributeFqn - ); + var autoAttribute = ctx.Attributes.First(); - if (attr is null || attr.ConstructorArguments.Length != 3) + if (autoAttribute is null || autoAttribute.ConstructorArguments.Length != 3) return null; - var id = attr.ConstructorArguments[0].Value as string; - var name = attr.ConstructorArguments[1].Value as string; - var version = attr.ConstructorArguments[2].Value as string; + var id = autoAttribute.ConstructorArguments[0].Value as string; + var name = autoAttribute.ConstructorArguments[1].Value as string; + var version = autoAttribute.ConstructorArguments[2].Value as string; var ns = typeSymbol.ContainingNamespace; var containingNamespace = ns.IsGlobalNamespace ? null : ns.ToDisplayString(); @@ -154,28 +165,32 @@ string autoAttributeFqn return new PluginClass( containingNamespace, typeSymbol.Name, - attributeFqn, new PluginProps(id, name, version) ); } - private void WriteClass( + static void WriteClass( SourceProductionContext context, - (PluginClass plugin, PluginProps pluginProps) source + (PluginClass plugin, PluginProps pluginProps) source, + string attribute ) { try { var (plugin, props) = source; + string pluginClass = plugin.ClassName; + StringBuilder sb = new(); sb.AppendLine("// "); - string? id = plugin.Overrides.Id ?? props.Id; - string? name = plugin.Overrides.Name ?? props.Name; - string? version = plugin.Overrides.Version ?? props.Version; - string pluginClass = plugin.ClassName; + string id = SymbolDisplay.FormatLiteral(plugin.Overrides.Id ?? props.Id!, true); + string name = SymbolDisplay.FormatLiteral(plugin.Overrides.Name ?? props.Name!, true); + string version = SymbolDisplay.FormatLiteral( + plugin.Overrides.Version ?? props.Version!, + true + ); if (plugin.Namespace is not null) { @@ -183,23 +198,23 @@ private void WriteClass( } var classStr = $$""" - [{{plugin.BepInAttribute}}({{pluginClass}}.Id, "{{name}}", "{{version}}")] + [global::{{attribute}}({{pluginClass}}.Id, {{name}}, {{version}})] partial class {{pluginClass}} { /// /// The Id of . /// - public const string Id = "{{id}}"; + public const string Id = {{id}}; /// /// Gets the name of . /// - public static string Name => "{{name}}"; + public static string Name => {{name}}; /// /// Gets the version of . /// - public static string Version => "{{version}}"; + public static string Version => {{version}}; } """; @@ -219,12 +234,7 @@ partial class {{pluginClass}} } } - public sealed record PluginClass( - string? Namespace, - string ClassName, - string BepInAttribute, - PluginProps Overrides - ); + public record struct PluginClass(string? Namespace, string ClassName, PluginProps Overrides); - public sealed record PluginProps(string? Id, string? Name, string? Version); + public record struct PluginProps(string? Id, string? Name, string? Version); } diff --git a/TestPlugin/TestPlugin.csproj b/TestPlugin/TestPlugin.csproj index 096613a..f337e66 100644 --- a/TestPlugin/TestPlugin.csproj +++ b/TestPlugin/TestPlugin.csproj @@ -2,7 +2,7 @@ test.TestPlugin - Test Plugin + "Test Plugin" 1.0.1-beta+69 net9.0 true From f079d147a3e370c8b01b87fa57cd3412f4a6634b Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 12:15:58 +0300 Subject: [PATCH 08/23] upgrade to latest CodeAnalysis references --- BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj index e4cfdd9..9c2efd7 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj @@ -20,8 +20,8 @@ - - + + all From 1f5b4431ad37626026f2135b832d69a686befd7f Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 12:18:20 +0300 Subject: [PATCH 09/23] don't generate for invalid classes --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 68f657b..f0fadbd 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -134,11 +134,14 @@ out var stripMetadata static bool IsValidPluginClass(SyntaxNode node) { - if (node is not ClassDeclarationSyntax { Parent: not TypeDeclarationSyntax }) + if (node is not ClassDeclarationSyntax { Parent: not TypeDeclarationSyntax } classSyntax) { return false; } + if (!classSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + return false; + return true; } From 3f6098f342d4174cb079d5dd38e25c8186e4c989 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 12:27:53 +0300 Subject: [PATCH 10/23] introduce analyzer for missing partial modifier --- .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 8 ++ .../Analyzers/PluginClassAnalyzer.cs | 83 +++++++++++++++++++ BepInEx.AutoPlugin/AutoPluginGenerator.cs | 4 +- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 BepInEx.AutoPlugin/AnalyzerReleases.Shipped.md create mode 100644 BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md create mode 100644 BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs diff --git a/BepInEx.AutoPlugin/AnalyzerReleases.Shipped.md b/BepInEx.AutoPlugin/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..60c1edf --- /dev/null +++ b/BepInEx.AutoPlugin/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/BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md b/BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..f902779 --- /dev/null +++ b/BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AutoPlugin0001 | BepInEx.AutoPlugin | Warning | AutoPluginAnalyzer \ No newline at end of file diff --git a/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs new file mode 100644 index 0000000..8a370e6 --- /dev/null +++ b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace BepInEx.AutoPlugin.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PluginClassAnalyzer : DiagnosticAnalyzer +{ + const string Category = "BepInEx.AutoPlugin"; + + public static readonly DiagnosticDescriptor PluginClassMustBeMarkedPartial = new( + "AutoPlugin0001", + "Plugin class must be marked partial", + "Plugin class '{0}' must be marked partial for use with AutoPlugin", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public override ImmutableArray SupportedDiagnostics { get; } = + [PluginClassMustBeMarkedPartial]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration); + } + + static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) + { + var classSyntax = (ClassDeclarationSyntax)context.Node; + + var semanticModel = context.SemanticModel; + var compilation = context.Compilation; + + INamedTypeSymbol?[] autoAttributes = + [ + compilation.GetTypeByMetadataName(AutoPluginGenerator.BepInAutoPluginAttribute), + compilation.GetTypeByMetadataName(AutoPluginGenerator.PatcherAutoPluginAttribute), + ]; + + var attributes = classSyntax.AttributeLists.SelectMany(attrList => attrList.Attributes); + var hasAutoPluginAttribute = attributes.Any(attribute => + { + if (semanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol) + return false; + + foreach (var autoAttribute in autoAttributes) + { + if ( + SymbolEqualityComparer.Default.Equals( + attributeSymbol.ContainingSymbol, + autoAttribute + ) + ) + { + return true; + } + } + + return false; + }); + + if (!hasAutoPluginAttribute) + return; + + if (classSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + return; + + var diagnostic = Diagnostic.Create( + PluginClassMustBeMarkedPartial, + classSyntax.Identifier.GetLocation(), + classSyntax.Identifier.ToString() + ); + + context.ReportDiagnostic(diagnostic); + } +} diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index f0fadbd..4fc07ae 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -8,8 +8,8 @@ namespace BepInEx.AutoPlugin; [Generator] public sealed class AutoPluginGenerator : IIncrementalGenerator { - const string BepInAutoPluginAttribute = "BepInEx.BepInAutoPluginAttribute"; - const string PatcherAutoPluginAttribute = + public const string BepInAutoPluginAttribute = "BepInEx.BepInAutoPluginAttribute"; + public const string PatcherAutoPluginAttribute = "BepInEx.Preloader.Core.Patching.PatcherAutoPluginAttribute"; const string BepInPluginAttribute = "BepInEx.BepInPlugin"; const string PatcherPluginInfoAttribute = "BepInEx.Preloader.Core.Patching.PatcherPluginInfo"; From d7e3b44bed2e57119c516ff183b56558ca75daa3 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 18:52:31 +0300 Subject: [PATCH 11/23] more proper nullable handling --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 4fc07ae..f863cc9 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -158,9 +158,9 @@ static bool IsValidPluginClass(SyntaxNode node) if (autoAttribute is null || autoAttribute.ConstructorArguments.Length != 3) return null; - var id = autoAttribute.ConstructorArguments[0].Value as string; - var name = autoAttribute.ConstructorArguments[1].Value as string; - var version = autoAttribute.ConstructorArguments[2].Value as string; + string? id = autoAttribute.ConstructorArguments[0].Value as string; + string? name = autoAttribute.ConstructorArguments[1].Value as string; + string? version = autoAttribute.ConstructorArguments[2].Value as string; var ns = typeSymbol.ContainingNamespace; var containingNamespace = ns.IsGlobalNamespace ? null : ns.ToDisplayString(); @@ -168,7 +168,7 @@ static bool IsValidPluginClass(SyntaxNode node) return new PluginClass( containingNamespace, typeSymbol.Name, - new PluginProps(id, name, version) + new PropOverrides(id, name, version) ); } @@ -182,18 +182,15 @@ string attribute { var (plugin, props) = source; - string pluginClass = plugin.ClassName; - StringBuilder sb = new(); sb.AppendLine("// "); - string id = SymbolDisplay.FormatLiteral(plugin.Overrides.Id ?? props.Id!, true); - string name = SymbolDisplay.FormatLiteral(plugin.Overrides.Name ?? props.Name!, true); - string version = SymbolDisplay.FormatLiteral( - plugin.Overrides.Version ?? props.Version!, - true - ); + PropOverrides overrides = plugin.Overrides; + string id = SymbolDisplay.FormatLiteral(overrides.Id ?? props.Id, true); + string name = SymbolDisplay.FormatLiteral(overrides.Name ?? props.Name, true); + string version = SymbolDisplay.FormatLiteral(overrides.Version ?? props.Version, true); + string pluginClass = plugin.ClassName; if (plugin.Namespace is not null) { @@ -237,7 +234,9 @@ partial class {{pluginClass}} } } - public record struct PluginClass(string? Namespace, string ClassName, PluginProps Overrides); + public record struct PluginClass(string? Namespace, string ClassName, PropOverrides Overrides); + + public record struct PropOverrides(string? Id, string? Name, string? Version); - public record struct PluginProps(string? Id, string? Name, string? Version); + public record struct PluginProps(string Id, string Name, string Version); } From 94ab940ca396979e8b6685520177501fbfe5b584 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 19:23:10 +0300 Subject: [PATCH 12/23] change ToPluginClass nullability handling --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index f863cc9..ecf9fb0 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -109,8 +109,7 @@ out var stripMetadata static (s, _) => IsValidPluginClass(s), static (ctx, _) => ToPluginClass(ctx) ) - .Where(x => x is not null) - .Select((x, _) => (PluginClass)x!); + .Where(x => x != default); context.RegisterSourceOutput( classesWithBepInAutoPlugin.Combine(pluginProps), @@ -123,8 +122,7 @@ out var stripMetadata static (s, _) => IsValidPluginClass(s), static (ctx, _) => ToPluginClass(ctx) ) - .Where(x => x is not null) - .Select((x, _) => (PluginClass)x!); + .Where(x => x != default); context.RegisterSourceOutput( classesWithPatcherAutoPlugin.Combine(pluginProps), @@ -145,18 +143,18 @@ static bool IsValidPluginClass(SyntaxNode node) return true; } - static PluginClass? ToPluginClass(GeneratorAttributeSyntaxContext ctx) + static PluginClass ToPluginClass(GeneratorAttributeSyntaxContext ctx) { var target = (ClassDeclarationSyntax)ctx.TargetNode; if (ctx.SemanticModel.GetDeclaredSymbol(target) is not INamedTypeSymbol typeSymbol) { - return null; + return default; } var autoAttribute = ctx.Attributes.First(); if (autoAttribute is null || autoAttribute.ConstructorArguments.Length != 3) - return null; + return default; string? id = autoAttribute.ConstructorArguments[0].Value as string; string? name = autoAttribute.ConstructorArguments[1].Value as string; From 0fbea64d6dd4e11db31da51c35f839760382adb6 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Thu, 24 Apr 2025 19:23:31 +0300 Subject: [PATCH 13/23] make record structs readonly --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index ecf9fb0..0bff8b7 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -232,9 +232,13 @@ partial class {{pluginClass}} } } - public record struct PluginClass(string? Namespace, string ClassName, PropOverrides Overrides); + public readonly record struct PluginClass( + string? Namespace, + string ClassName, + PropOverrides Overrides + ); - public record struct PropOverrides(string? Id, string? Name, string? Version); + public readonly record struct PropOverrides(string? Id, string? Name, string? Version); - public record struct PluginProps(string Id, string Name, string Version); + public readonly record struct PluginProps(string Id, string Name, string Version); } From a0d9942977c48d85bd31bc1e50964c17862103ba Mon Sep 17 00:00:00 2001 From: Hamunii Date: Fri, 25 Apr 2025 13:30:50 +0300 Subject: [PATCH 14/23] downgrade Microsoft.CodeAnalysis.CSharp to oldest supported version --- BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj index 9c2efd7..4364a15 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj @@ -20,7 +20,7 @@ - + From 09d2f617abf719df64092e2ac4ada1ff6bc98e96 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Fri, 25 Apr 2025 13:34:14 +0300 Subject: [PATCH 15/23] this needs to be changed due to the downgraded version, whoops --- BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs index 8a370e6..386a24b 100644 --- a/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs +++ b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs @@ -21,7 +21,7 @@ public sealed class PluginClassAnalyzer : DiagnosticAnalyzer ); public override ImmutableArray SupportedDiagnostics { get; } = - [PluginClassMustBeMarkedPartial]; + ImmutableArray.Create(PluginClassMustBeMarkedPartial); public override void Initialize(AnalysisContext context) { From 1ce922b4baafadaf31e369d36918ec57eb974d13 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Fri, 25 Apr 2025 15:02:08 +0300 Subject: [PATCH 16/23] use ds5678's method for analyzer --- .../Analyzers/PluginClassAnalyzer.cs | 95 +++++++++++-------- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 26 +++-- 2 files changed, 74 insertions(+), 47 deletions(-) diff --git a/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs index 386a24b..8f8a1c3 100644 --- a/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs +++ b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs @@ -28,56 +28,77 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); - context.RegisterSyntaxNodeAction(AnalyzeClassDeclaration, SyntaxKind.ClassDeclaration); + context.RegisterSymbolAction(AnalyzeClassSymbol, SymbolKind.NamedType); } - static void AnalyzeClassDeclaration(SyntaxNodeAnalysisContext context) + static void AnalyzeClassSymbol(SymbolAnalysisContext context) { - var classSyntax = (ClassDeclarationSyntax)context.Node; + var typeSymbol = (INamedTypeSymbol)context.Symbol; - var semanticModel = context.SemanticModel; - var compilation = context.Compilation; - - INamedTypeSymbol?[] autoAttributes = - [ - compilation.GetTypeByMetadataName(AutoPluginGenerator.BepInAutoPluginAttribute), - compilation.GetTypeByMetadataName(AutoPluginGenerator.PatcherAutoPluginAttribute), - ]; + bool isNotClassOrIsPartialClass = typeSymbol.DeclaringSyntaxReferences.All( + static syntaxReference => + syntaxReference.GetSyntax() is not ClassDeclarationSyntax classSyntax + || classSyntax.Modifiers.Any(SyntaxKind.PartialKeyword) + ); - var attributes = classSyntax.AttributeLists.SelectMany(attrList => attrList.Attributes); - var hasAutoPluginAttribute = attributes.Any(attribute => + if (isNotClassOrIsPartialClass) { - if (semanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol) - return false; + return; + } - foreach (var autoAttribute in autoAttributes) + foreach (AttributeData attribute in typeSymbol.GetAttributes()) + { + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) { - if ( - SymbolEqualityComparer.Default.Equals( - attributeSymbol.ContainingSymbol, - autoAttribute - ) - ) - { - return true; - } + continue; } - return false; - }); + switch (attributeClass.Name) + { + case AutoPluginGenerator.BepInAutoPluginAttributeName: + string attributeFullName = attributeClass.ToDisplayString(); + if (attributeFullName != AutoPluginGenerator.BepInAutoPluginAttributeFullName) + { + continue; + } + break; + + case AutoPluginGenerator.PatcherAutoPluginAttributeName: + attributeFullName = attributeClass.ToDisplayString(); + if (attributeFullName != AutoPluginGenerator.PatcherAutoPluginAttributeFullName) + { + continue; + } + break; + + default: + continue; + } - if (!hasAutoPluginAttribute) - return; + if ( + attribute.ApplicationSyntaxReference?.GetSyntax() + is not AttributeSyntax attributeSyntax + ) + { + continue; + } - if (classSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) - return; + if ( + attributeSyntax.Parent is not AttributeListSyntax attributeList + || attributeList.Parent is not ClassDeclarationSyntax classDeclaration + ) + { + continue; + } - var diagnostic = Diagnostic.Create( - PluginClassMustBeMarkedPartial, - classSyntax.Identifier.GetLocation(), - classSyntax.Identifier.ToString() - ); + var diagnostic = Diagnostic.Create( + PluginClassMustBeMarkedPartial, + classDeclaration.Identifier.GetLocation(), + classDeclaration.Identifier.ToString() + ); - context.ReportDiagnostic(diagnostic); + context.ReportDiagnostic(diagnostic); + } } } diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 0bff8b7..aec00cc 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -8,21 +8,27 @@ namespace BepInEx.AutoPlugin; [Generator] public sealed class AutoPluginGenerator : IIncrementalGenerator { - public const string BepInAutoPluginAttribute = "BepInEx.BepInAutoPluginAttribute"; - public const string PatcherAutoPluginAttribute = - "BepInEx.Preloader.Core.Patching.PatcherAutoPluginAttribute"; + public const string BepInAutoPluginAttributeName = "BepInAutoPluginAttribute"; + public const string PatcherAutoPluginAttributeName = "PatcherAutoPluginAttribute"; + + public const string BepInAutoPluginAttributeFullName = + "BepInEx." + BepInAutoPluginAttributeName; + public const string PatcherAutoPluginAttributeFullName = + "BepInEx.Preloader.Core.Patching." + PatcherAutoPluginAttributeName; + const string BepInPluginAttribute = "BepInEx.BepInPlugin"; const string PatcherPluginInfoAttribute = "BepInEx.Preloader.Core.Patching.PatcherPluginInfo"; - const string AttributeCode = """ + + const string AttributeCode = $$""" // #nullable enable namespace BepInEx { [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [global::System.Diagnostics.Conditional("CodeGeneration")] - internal sealed class BepInAutoPluginAttribute : global::System.Attribute + internal sealed class {{BepInAutoPluginAttributeName}} : global::System.Attribute { - public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) {} + public {{BepInAutoPluginAttributeName}}(string? id = null, string? name = null, string? version = null) {} } } @@ -30,9 +36,9 @@ namespace BepInEx.Preloader.Core.Patching { [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [global::System.Diagnostics.Conditional("CodeGeneration")] - internal sealed class PatcherAutoPluginAttribute : global::System.Attribute + internal sealed class {{PatcherAutoPluginAttributeName}} : global::System.Attribute { - public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) {} + public {{PatcherAutoPluginAttributeName}}(string? id = null, string? name = null, string? version = null) {} } } """; @@ -105,7 +111,7 @@ out var stripMetadata IncrementalValuesProvider classesWithBepInAutoPlugin = context .SyntaxProvider.ForAttributeWithMetadataName( - BepInAutoPluginAttribute, + BepInAutoPluginAttributeFullName, static (s, _) => IsValidPluginClass(s), static (ctx, _) => ToPluginClass(ctx) ) @@ -118,7 +124,7 @@ out var stripMetadata IncrementalValuesProvider classesWithPatcherAutoPlugin = context .SyntaxProvider.ForAttributeWithMetadataName( - PatcherAutoPluginAttribute, + PatcherAutoPluginAttributeFullName, static (s, _) => IsValidPluginClass(s), static (ctx, _) => ToPluginClass(ctx) ) From 872ad754c0d7666dafcefea8acd4bc34a309a4d2 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Fri, 25 Apr 2025 15:32:09 +0300 Subject: [PATCH 17/23] cleanup - remove BepInAutoPluginStripBuildMetadata option since it's not actually needed when taking properties from msbuild - made a method to get rid of essentially duplicate code for getting plugin classes - moved comment about BepInEx 5 specific quirks to a more logical place --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 72 +++++++++------------ BepInEx.AutoPlugin/BepInEx.AutoPlugin.props | 1 - TestPlugin/TestPlugin.csproj | 1 - 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index aec00cc..61227bc 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -54,16 +54,15 @@ public void Initialize(IncrementalGeneratorInitializationContext context) (compilation, _) => compilation.ReferencedAssemblyNames ); + // BepInEx 5 doesn't support plugins having version pre-release or build metadata + // such as 1.0.0-beta or 1.0.0+97ec53d20958b88581680d4d3c15ba59a8900ed5 + // so even if the user sets those in the project Version property, + // we need to strip them out. IncrementalValueProvider isBepInEx5 = references.Select( (refs, _) => { var bepInEx = refs.FirstOrDefault(r => r.Name.Equals("BepInEx")); - - // BepInEx 5 doesn't support plugins having version pre-release or build metadata - // such as 1.0.0-beta or 1.0.0+97ec53d20958b88581680d4d3c15ba59a8900ed5 - bool isBepInEx5 = bepInEx is not null && bepInEx.Version.Major == 5; - - return isBepInEx5; + return bepInEx is not null && bepInEx.Version.Major == 5; } ); @@ -73,62 +72,37 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (info, _) => { var (options, isBepInEx5) = info; - options.GlobalOptions.TryGetValue( - "build_property.AssemblyName", - out var assemblyName - ); - options.GlobalOptions.TryGetValue( - "build_property.Product", - out var productName - ); - options.GlobalOptions.TryGetValue("build_property.Version", out var version); - options.GlobalOptions.TryGetValue( - "build_property.BepInAutoPluginStripBuildMetadata", - out var stripMetadata - ); - - // Afaik these should always be set. + var globalOptions = options.GlobalOptions; + globalOptions.TryGetValue("build_property.AssemblyName", out var assemblyName); + globalOptions.TryGetValue("build_property.Product", out var productName); + globalOptions.TryGetValue("build_property.Version", out var version); + + // These values are from the project properties + // and as such should always be set to at least default values by the SDK + // unless if the user doesn't reference 'build' assets from our package. assemblyName ??= "unknown"; productName ??= "unknown"; version ??= "0.0.0.0"; - bool isStripMetadata = - stripMetadata?.Equals("true", StringComparison.InvariantCultureIgnoreCase) - ?? false; - if (isBepInEx5) { version = version.Split('-', '+')[0]; } - else if (isStripMetadata) - { - version = version.Split('+')[0]; - } return new PluginProps(assemblyName, productName, version); } ); - IncrementalValuesProvider classesWithBepInAutoPlugin = context - .SyntaxProvider.ForAttributeWithMetadataName( - BepInAutoPluginAttributeFullName, - static (s, _) => IsValidPluginClass(s), - static (ctx, _) => ToPluginClass(ctx) - ) - .Where(x => x != default); + IncrementalValuesProvider classesWithBepInAutoPlugin = + GetPluginClassesWithAttribute(context, BepInAutoPluginAttributeFullName); context.RegisterSourceOutput( classesWithBepInAutoPlugin.Combine(pluginProps), static (context, source) => WriteClass(context, source, BepInPluginAttribute) ); - IncrementalValuesProvider classesWithPatcherAutoPlugin = context - .SyntaxProvider.ForAttributeWithMetadataName( - PatcherAutoPluginAttributeFullName, - static (s, _) => IsValidPluginClass(s), - static (ctx, _) => ToPluginClass(ctx) - ) - .Where(x => x != default); + IncrementalValuesProvider classesWithPatcherAutoPlugin = + GetPluginClassesWithAttribute(context, PatcherAutoPluginAttributeFullName); context.RegisterSourceOutput( classesWithPatcherAutoPlugin.Combine(pluginProps), @@ -136,6 +110,18 @@ out var stripMetadata ); } + static IncrementalValuesProvider GetPluginClassesWithAttribute( + IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName + ) => + context + .SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate: static (s, _) => IsValidPluginClass(s), + transform: static (ctx, _) => ToPluginClass(ctx) + ) + .Where(x => x != default); + static bool IsValidPluginClass(SyntaxNode node) { if (node is not ClassDeclarationSyntax { Parent: not TypeDeclarationSyntax } classSyntax) diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props index 06b7716..804341c 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props @@ -3,6 +3,5 @@ - diff --git a/TestPlugin/TestPlugin.csproj b/TestPlugin/TestPlugin.csproj index f337e66..9fa6309 100644 --- a/TestPlugin/TestPlugin.csproj +++ b/TestPlugin/TestPlugin.csproj @@ -6,7 +6,6 @@ 1.0.1-beta+69 net9.0 true - true From e62cd4bd46e7cbd0cff02d2f3f946eff90b5fd31 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Fri, 25 Apr 2025 16:18:51 +0300 Subject: [PATCH 18/23] update readme to be more comprehensive --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2574f36..a7ac658 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,21 @@ [![CI](https://github.com/BepInEx/BepInEx.AutoPlugin/workflows/CI/badge.svg)](https://github.com/BepInEx/BepInEx.AutoPlugin/actions) [![NuGet](https://img.shields.io/endpoint?color=blue&logo=NuGet&label=NuGet&url=https://shields.kzu.io/v/BepInEx.AutoPlugin?feed=nuget.bepinex.dev/v3/index.json)](https://nuget.bepinex.dev/packages/BepInEx.AutoPlugin) -Incremental C# Source generator that turns +BepInEx.AutoPlugin is an incremental C# source generator that takes the following properties from your project: -```cs -[BepInAutoPlugin("com.example.ExamplePlugin")] -public partial class ExamplePlugin : BaseUnityPlugin -{ -} +```xml + + com.example.ExamplePlugin + ExamplePlugin + 0.1.0 + ``` -into +And generates a partial class for your partial plugin class decorated with the `BepInAutoPluginAttribute`, decorating the generated class with the `BepInPluginAttribute` using the above properties: ```cs [BepInEx.BepInPlugin(ExamplePlugin.Id, "ExamplePlugin", "0.1.0")] -public class ExamplePlugin : BaseUnityPlugin +partial class ExamplePlugin : BaseUnityPlugin { public const string Id = "com.example.ExamplePlugin"; public static string Name => "ExamplePlugin"; @@ -24,14 +25,41 @@ public class ExamplePlugin : BaseUnityPlugin } ``` -## Configuration +A `PatcherAutoPluginAttribute` also exists for BepInEx 6 preloader patchers. -AutoPlugin allows stripping version build metadata to turn `1.0.0-beta+1234567890` into `1.0.0-beta` for the `Version` property by setting the following in your csproj: +## Usage -```xml - - true - +Mark your plugin class partial, and decorate it with the `BepInAutoPluginAttribute`: + +```cs +[BepInAutoPlugin] +public partial class ExamplePlugin : BaseUnityPlugin +{ +} ``` -Note that this will do nothing on BepInEx 5 as it only accepts a version number without a pre-release version or build metadata, so it strips both. +And you're done! You don't need to manually include the `BepInPluginAttribute`, as the generated partial class includes that. + +You can also access all the properties in your code, as they are public members in your class: + +```cs +[BepInAutoPlugin] +public partial class ExamplePlugin : BaseUnityPlugin +{ + void Awake() + { + Logger.LogInfo($"Plugin {Name} version {Version} is loaded!"); + } +} +``` + +### Overriding Properties + +AutoPlugin allows overriding any of the properties with the optional attribute arguments: + +```cs +[BepInAutoPlugin(id: "com.example.MyOverrideId", name: "My Override Name", version: "1.2.3")] +public partial class ExamplePlugin : BaseUnityPlugin +{ +} +``` From 211dfb00753130565dd124e2c4aa502360a4ceaa Mon Sep 17 00:00:00 2001 From: Hamunii Date: Sat, 26 Apr 2025 08:01:36 +0300 Subject: [PATCH 19/23] use Title for project name with Product as a fallback --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 15 +++++++++++---- BepInEx.AutoPlugin/BepInEx.AutoPlugin.props | 1 + README.md | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 61227bc..7866c0d 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -74,14 +74,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var (options, isBepInEx5) = info; var globalOptions = options.GlobalOptions; globalOptions.TryGetValue("build_property.AssemblyName", out var assemblyName); - globalOptions.TryGetValue("build_property.Product", out var productName); + globalOptions.TryGetValue("build_property.Title", out var projectName); globalOptions.TryGetValue("build_property.Version", out var version); + // 'Product' is always defined, use it as a fallback. + if (string.IsNullOrEmpty(projectName)) + { + globalOptions.TryGetValue("build_property.Product", out projectName); + } + // These values are from the project properties // and as such should always be set to at least default values by the SDK - // unless if the user doesn't reference 'build' assets from our package. + // unless if the user doesn't reference 'build' assets from our package, + // in which case they are null. assemblyName ??= "unknown"; - productName ??= "unknown"; + projectName ??= "unknown"; version ??= "0.0.0.0"; if (isBepInEx5) @@ -89,7 +96,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) version = version.Split('-', '+')[0]; } - return new PluginProps(assemblyName, productName, version); + return new PluginProps(assemblyName, projectName, version); } ); diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props index 804341c..433771b 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props @@ -1,6 +1,7 @@ + diff --git a/README.md b/README.md index a7ac658..815feec 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ BepInEx.AutoPlugin is an incremental C# source generator that takes the followin ```xml com.example.ExamplePlugin - ExamplePlugin + ExamplePlugin 0.1.0 ``` From 808b719c9975d6302e70014179ad860056640767 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Sat, 26 Apr 2025 17:06:55 +0300 Subject: [PATCH 20/23] take info from assembly attributes instead & reintroduce BepInAutoPluginStripBuildMetadata option it turns out trying to take the info from MSBuild properties isn't reliable because why would it be. --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 100 +++++++++++++++----- BepInEx.AutoPlugin/BepInEx.AutoPlugin.props | 5 +- TestPlugin/TestPlugin.csproj | 3 +- 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 7866c0d..3910943 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -45,13 +45,33 @@ internal sealed class {{PatcherAutoPluginAttributeName}} : global::System.Attrib public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterPostInitializationOutput(ctx => + context.RegisterPostInitializationOutput(static ctx => { ctx.AddSource("BepInAutoPluginAttribute.g.cs", AttributeCode); }); + // First, collect all the data we might need + IncrementalValueProvider assemblyName = context.CompilationProvider.Select( + (compilation, _) => compilation.AssemblyName + ); + + IncrementalValueProvider assemblyTitle = GetStringFromUniqueAttribute( + context, + "System.Reflection.AssemblyTitleAttribute" + ); + + IncrementalValueProvider informationalVersion = GetStringFromUniqueAttribute( + context, + "System.Reflection.AssemblyInformationalVersionAttribute" + ); + + IncrementalValueProvider version = GetStringFromUniqueAttribute( + context, + "System.Reflection.AssemblyVersionAttribute" + ); + var references = context.CompilationProvider.Select( - (compilation, _) => compilation.ReferencedAssemblyNames + static (compilation, _) => compilation.ReferencedAssemblyNames ); // BepInEx 5 doesn't support plugins having version pre-release or build metadata @@ -59,44 +79,60 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // so even if the user sets those in the project Version property, // we need to strip them out. IncrementalValueProvider isBepInEx5 = references.Select( - (refs, _) => + static (refs, _) => { var bepInEx = refs.FirstOrDefault(r => r.Name.Equals("BepInEx")); return bepInEx is not null && bepInEx.Version.Major == 5; } ); - IncrementalValueProvider pluginProps = context - .AnalyzerConfigOptionsProvider.Combine(isBepInEx5) - .Select( - static (info, _) => + IncrementalValueProvider shouldStripInformationalVersionBuildMetadata = + context.AnalyzerConfigOptionsProvider.Select( + static (options, _) => { - var (options, isBepInEx5) = info; var globalOptions = options.GlobalOptions; - globalOptions.TryGetValue("build_property.AssemblyName", out var assemblyName); - globalOptions.TryGetValue("build_property.Title", out var projectName); - globalOptions.TryGetValue("build_property.Version", out var version); + globalOptions.TryGetValue( + "build_property.BepInAutoPluginStripBuildMetadata", + out var stripMetadata + ); - // 'Product' is always defined, use it as a fallback. - if (string.IsNullOrEmpty(projectName)) - { - globalOptions.TryGetValue("build_property.Product", out projectName); - } + bool shouldStripMetadata = + stripMetadata?.Equals("true", StringComparison.InvariantCultureIgnoreCase) + ?? false; - // These values are from the project properties - // and as such should always be set to at least default values by the SDK - // unless if the user doesn't reference 'build' assets from our package, - // in which case they are null. - assemblyName ??= "unknown"; - projectName ??= "unknown"; - version ??= "0.0.0.0"; + return shouldStripMetadata; + } + ); + + // Now we have everything, combine and process the data + IncrementalValueProvider pluginProps = assemblyName + .Combine(assemblyTitle) + .Combine(informationalVersion) + .Combine(version) + .Combine(isBepInEx5) + .Combine(shouldStripInformationalVersionBuildMetadata) + .Select( + static (items, _) => + { + var ( + ((((assemblyName, title), informationalVersion), version), isBepInEx5), + shouldStripMetadata + ) = items; + + string projectId = assemblyName ??= "unknown"; + string projectName = title ?? projectId; + string projectVersion = informationalVersion ?? version ?? "0.0.0.0"; if (isBepInEx5) { - version = version.Split('-', '+')[0]; + projectVersion = projectVersion.Split('-', '+')[0]; + } + else if (shouldStripMetadata) + { + projectVersion = projectVersion.Split('+')[0]; } - return new PluginProps(assemblyName, projectName, version); + return new PluginProps(projectId, projectName, projectVersion); } ); @@ -117,6 +153,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ); } + static IncrementalValueProvider GetStringFromUniqueAttribute( + IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName + ) => + context + .SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate: static (s, _) => true, + transform: static (ctx, _) => + ctx.Attributes[0].ConstructorArguments[0].Value as string + ) + .Collect() + .Select((x, _) => x.FirstOrDefault()); + static IncrementalValuesProvider GetPluginClassesWithAttribute( IncrementalGeneratorInitializationContext context, string fullyQualifiedMetadataName diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props index 433771b..57f4cac 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props @@ -1,8 +1,5 @@ - - - - + diff --git a/TestPlugin/TestPlugin.csproj b/TestPlugin/TestPlugin.csproj index 9fa6309..cd62627 100644 --- a/TestPlugin/TestPlugin.csproj +++ b/TestPlugin/TestPlugin.csproj @@ -2,10 +2,11 @@ test.TestPlugin - "Test Plugin" + "Test Plugin" 1.0.1-beta+69 net9.0 true + true From 1793b017670882195f5249666637098214a23890 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Sat, 26 Apr 2025 17:11:44 +0300 Subject: [PATCH 21/23] update readme to be up to date again --- README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 815feec..7973743 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ BepInEx.AutoPlugin is an incremental C# source generator that takes the followin ```xml com.example.ExamplePlugin - ExamplePlugin + ExamplePlugin 0.1.0 ``` @@ -63,3 +63,15 @@ public partial class ExamplePlugin : BaseUnityPlugin { } ``` + +### MSBuild Configuration + +AutoPlugin allows stripping version build metadata to turn `1.0.0-beta+1234567890` into `1.0.0-beta` for the `Version` property by setting the following in your csproj: + +```xml + + true + +``` + +Note that this will do nothing on BepInEx 5 as it only accepts a version number without a pre-release version or build metadata, so it strips both. From d7961dc69cd2e064ce9921453c0001c94bf7dbc5 Mon Sep 17 00:00:00 2001 From: Hamunii Date: Tue, 29 Apr 2025 23:40:52 +0300 Subject: [PATCH 22/23] switch from using FAWMN for getting attribute arguments This is probably slightly faster than using ForAttributeWithMetadataName based on my terrible benchmarks, and where FAWMN shines is attributes that don't exist, but these attributes should always exist. --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 98 +++++++++++++++-------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 3910943..ef92f7c 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -51,23 +51,72 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); // First, collect all the data we might need - IncrementalValueProvider assemblyName = context.CompilationProvider.Select( - (compilation, _) => compilation.AssemblyName - ); + var assemblyData = context.CompilationProvider.Select( + static (compilation, _) => + { + // This might be a little faster than + // ForAttributeWithMetadataName for what we're doing + // since the attributes should always exist. + var attributes = compilation.Assembly.GetAttributes(); - IncrementalValueProvider assemblyTitle = GetStringFromUniqueAttribute( - context, - "System.Reflection.AssemblyTitleAttribute" - ); + string? title = null; + string? informationalVersion = null; + string? version = null; - IncrementalValueProvider informationalVersion = GetStringFromUniqueAttribute( - context, - "System.Reflection.AssemblyInformationalVersionAttribute" - ); + for (int i = 0; i < attributes.Length; i++) + { + var attribute = attributes[i]; + + var attributeClass = attribute.AttributeClass; + + if (attributeClass == null) + { + continue; + } - IncrementalValueProvider version = GetStringFromUniqueAttribute( - context, - "System.Reflection.AssemblyVersionAttribute" + if (attribute.ConstructorArguments.Length != 1) + continue; + + string? expectedSystemNamespace = attributeClass + .ContainingNamespace + ?.ContainingNamespace + ?.MetadataName; + + if (expectedSystemNamespace != "System") + continue; + + var expectedReflectionNamespace = attributeClass + .ContainingNamespace! + .MetadataName; + + if (expectedReflectionNamespace != "Reflection") + continue; + + var className = attributeClass.MetadataName; + + if (className == "AssemblyTitleAttribute") + { + title = attribute.ConstructorArguments[0].Value as string; + } + else if (className == "AssemblyInformationalVersionAttribute") + { + informationalVersion = attribute.ConstructorArguments[0].Value as string; + } + else if (className == "AssemblyVersionAttribute") + { + version = attribute.ConstructorArguments[0].Value as string; + } + + if (title is not null && informationalVersion is not null) + { + // We did not check that version is not null because + // we'll use informationalVersion over version anyways. + break; + } + } + + return (compilation.AssemblyName, title, informationalVersion, version); + } ); var references = context.CompilationProvider.Select( @@ -105,17 +154,14 @@ out var stripMetadata ); // Now we have everything, combine and process the data - IncrementalValueProvider pluginProps = assemblyName - .Combine(assemblyTitle) - .Combine(informationalVersion) - .Combine(version) + IncrementalValueProvider pluginProps = assemblyData .Combine(isBepInEx5) .Combine(shouldStripInformationalVersionBuildMetadata) .Select( static (items, _) => { var ( - ((((assemblyName, title), informationalVersion), version), isBepInEx5), + ((assemblyName, title, informationalVersion, version), isBepInEx5), shouldStripMetadata ) = items; @@ -153,20 +199,6 @@ out var stripMetadata ); } - static IncrementalValueProvider GetStringFromUniqueAttribute( - IncrementalGeneratorInitializationContext context, - string fullyQualifiedMetadataName - ) => - context - .SyntaxProvider.ForAttributeWithMetadataName( - fullyQualifiedMetadataName, - predicate: static (s, _) => true, - transform: static (ctx, _) => - ctx.Attributes[0].ConstructorArguments[0].Value as string - ) - .Collect() - .Select((x, _) => x.FirstOrDefault()); - static IncrementalValuesProvider GetPluginClassesWithAttribute( IncrementalGeneratorInitializationContext context, string fullyQualifiedMetadataName From a7e309c26273564c1a1572e5083239d295cbbf6a Mon Sep 17 00:00:00 2001 From: Hamunii Date: Wed, 25 Jun 2025 02:37:02 +0300 Subject: [PATCH 23/23] ignore all warnings in generated file it's probably better to just ignore everything since the user can't do anything about it if they have some analyzer that that doesn't like what's happening there. One issue is when publicizing an assembly which also uses AutoPlugin. --- BepInEx.AutoPlugin/AutoPluginGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index ef92f7c..1f6f1db 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -264,6 +264,7 @@ string attribute StringBuilder sb = new(); sb.AppendLine("// "); + sb.AppendLine("#pragma warning disable"); PropOverrides overrides = plugin.Overrides; string id = SymbolDisplay.FormatLiteral(overrides.Id ?? props.Id, true);