From 99ce352496640996da9c596c3dcbdf755553cd2a Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Fri, 16 Feb 2024 18:34:20 -0500 Subject: [PATCH 1/4] Changed the source generator to use the hoist pattern Instead of having the users source generator be the entry point we create the entrypoint. This way we can 100% make sure the assembly resolver loaded before the users types get loaded. The main problem was that if users source generator contained static fields of types contained within external assemblies this would explode because the assembly resolver had not be subscribed yet --- src/Directory.Build.props | 3 - src/Nuget.Build.props | 4 - ...SourceGenerator.Foundations.Windows.csproj | 1 - .../WindowsDevelopmentPlatform.cs | 5 +- .../ConsoleApp.SourceGenerator.csproj | 4 +- .../ConsoleAppSourceGenerator.cs | 10 +- src/Sandbox/ConsoleApp/ConsoleApp.csproj | 1 + .../GenericDevelopmentPlatform.cs | 11 +- ...ntPlatform.cs => IGeneratorEnvironment.cs} | 10 +- .../IncrementalGenerator.cs | 90 ++++++++ .../SgfGeneratorAttribute.cs | 14 ++ .../SgfInitializationContext.cs | 131 ++++++++++++ .../SgfSourceProductionContext.cs | 66 ++++++ ...urceGenerator.Foundations.Contracts.csproj | 10 + .../ErrorFactory.cs | 55 ----- .../InjectionException.cs | 7 - .../Injector.cs | 145 ------------- .../Program.cs | 45 ---- .../Properties/launchSettings.json | 8 - ...ourceGenerator.Foundations.Injector.csproj | 11 - ...urceGenerator.Foundations.Shared.projitems | 2 +- src/SourceGenerator.Foundations.sln | 18 -- .../HoistSourceGenerator.cs | 87 ++++++++ .../Models/SourceGeneratorDataModel.cs | 37 ++++ .../Properties/launchSettings.json | 1 - .../SourceGenerator.Foundations.csproj | 10 +- .../SourceGenerator.Foundations.targets | 4 +- .../Templates/AssemblyResolver.cs | 195 ++++++++++++++++++ .../Templates/SourceGeneratorHoistBase.cs | 136 ++++++++++++ .../Templates/SourceGeneratorHostImpl.cs | 91 ++++++++ 30 files changed, 894 insertions(+), 318 deletions(-) rename src/SourceGenerator.Foundations.Contracts/{Platforms => Environments}/GenericDevelopmentPlatform.cs (59%) rename src/SourceGenerator.Foundations.Contracts/{IDevelopmentPlatform.cs => IGeneratorEnvironment.cs} (61%) create mode 100644 src/SourceGenerator.Foundations.Contracts/IncrementalGenerator.cs create mode 100644 src/SourceGenerator.Foundations.Contracts/SgfGeneratorAttribute.cs create mode 100644 src/SourceGenerator.Foundations.Contracts/SgfInitializationContext.cs create mode 100644 src/SourceGenerator.Foundations.Contracts/SgfSourceProductionContext.cs delete mode 100644 src/SourceGenerator.Foundations.Injector/ErrorFactory.cs delete mode 100644 src/SourceGenerator.Foundations.Injector/InjectionException.cs delete mode 100644 src/SourceGenerator.Foundations.Injector/Injector.cs delete mode 100644 src/SourceGenerator.Foundations.Injector/Program.cs delete mode 100644 src/SourceGenerator.Foundations.Injector/Properties/launchSettings.json delete mode 100644 src/SourceGenerator.Foundations.Injector/SourceGenerator.Foundations.Injector.csproj create mode 100644 src/SourceGenerator.Foundations/HoistSourceGenerator.cs create mode 100644 src/SourceGenerator.Foundations/Models/SourceGeneratorDataModel.cs create mode 100644 src/SourceGenerator.Foundations/Templates/AssemblyResolver.cs create mode 100644 src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs create mode 100644 src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 120e862..3d861d5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -17,9 +17,6 @@ $(SGFSourceDir)SourceGenerator.Foundations.Contracts\ $(SGFContractsProjectDir)SourceGenerator.Foundations.Contracts.csproj - - $(SGFSourceDir)SourceGenerator.Foundations.Injector\ - $(SGFInjectorProjectDir)SourceGenerator.Foundations.Injector.csproj $(SGFSourceDir)SourceGenerator.Foundations.MSBuild\ $(SGFMSBuildProjectDir)SourceGenerator.Foundations.MSBuild.csproj diff --git a/src/Nuget.Build.props b/src/Nuget.Build.props index 671ecae..a28ec08 100644 --- a/src/Nuget.Build.props +++ b/src/Nuget.Build.props @@ -20,7 +20,6 @@ - lib/netstandard2.0/SourceGenerator.Foundations.Contracts.dll @@ -31,9 +30,6 @@ sgf/injector/ - - build - \ No newline at end of file diff --git a/src/Plugins/SourceGenerator.Foundations.Windows/SourceGenerator.Foundations.Windows.csproj b/src/Plugins/SourceGenerator.Foundations.Windows/SourceGenerator.Foundations.Windows.csproj index 4ac249c..25f6461 100644 --- a/src/Plugins/SourceGenerator.Foundations.Windows/SourceGenerator.Foundations.Windows.csproj +++ b/src/Plugins/SourceGenerator.Foundations.Windows/SourceGenerator.Foundations.Windows.csproj @@ -13,6 +13,5 @@ - diff --git a/src/Plugins/SourceGenerator.Foundations.Windows/WindowsDevelopmentPlatform.cs b/src/Plugins/SourceGenerator.Foundations.Windows/WindowsDevelopmentPlatform.cs index 3d92ad0..0256cf5 100644 --- a/src/Plugins/SourceGenerator.Foundations.Windows/WindowsDevelopmentPlatform.cs +++ b/src/Plugins/SourceGenerator.Foundations.Windows/WindowsDevelopmentPlatform.cs @@ -9,13 +9,16 @@ namespace SGF /// /// Represents a enviroment where the user is authoring code in Visual Studio /// - internal class WindowsDevelopmentPlatform : IDevelopmentPlatform + internal class WindowsDevelopmentPlatform : IGeneratorEnvironment { public PlatformType Type { get; } + public string Name { get; } + public WindowsDevelopmentPlatform() { + Name = "VisualStudio"; Type = PlatformType.VisualStudio; if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("VisualStudioVersion"))) diff --git a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleApp.SourceGenerator.csproj b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleApp.SourceGenerator.csproj index c80ed1f..db95b59 100644 --- a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleApp.SourceGenerator.csproj +++ b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleApp.SourceGenerator.csproj @@ -5,6 +5,7 @@ true AnyCPU;x64 true + true @@ -12,7 +13,8 @@ + + - diff --git a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs index 8fad108..5148d6e 100644 --- a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs +++ b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs @@ -6,7 +6,7 @@ namespace ConsoleApp.SourceGenerator { - [Generator] + [SgfGenerator] internal class ConsoleAppSourceGenerator : IncrementalGenerator { public class Payload @@ -15,10 +15,12 @@ public class Payload public string? Version { get; set; } } - public ConsoleAppSourceGenerator() : base("ConsoleAppSourceGenerator") - { } + public ConsoleAppSourceGenerator(IGeneratorEnvironment generatorPlatform, ILogger logger) : base("ConsoleAppSourceGenerator", generatorPlatform, logger) + { + + } - protected override void OnInitialize(SgfInitializationContext context) + public override void OnInitialize(SgfInitializationContext context) { Payload payload = new() { diff --git a/src/Sandbox/ConsoleApp/ConsoleApp.csproj b/src/Sandbox/ConsoleApp/ConsoleApp.csproj index f9c7ee0..1d0d95d 100644 --- a/src/Sandbox/ConsoleApp/ConsoleApp.csproj +++ b/src/Sandbox/ConsoleApp/ConsoleApp.csproj @@ -3,6 +3,7 @@ Exe net6.0 enable + true diff --git a/src/SourceGenerator.Foundations.Contracts/Platforms/GenericDevelopmentPlatform.cs b/src/SourceGenerator.Foundations.Contracts/Environments/GenericDevelopmentPlatform.cs similarity index 59% rename from src/SourceGenerator.Foundations.Contracts/Platforms/GenericDevelopmentPlatform.cs rename to src/SourceGenerator.Foundations.Contracts/Environments/GenericDevelopmentPlatform.cs index cce2cd9..6bf0930 100644 --- a/src/SourceGenerator.Foundations.Contracts/Platforms/GenericDevelopmentPlatform.cs +++ b/src/SourceGenerator.Foundations.Contracts/Environments/GenericDevelopmentPlatform.cs @@ -3,10 +3,17 @@ using System.Collections.Generic; using System.Diagnostics; -namespace SGF.Platforms +namespace SGF.Environments { - internal class GenericDevelopmentPlatform : IDevelopmentPlatform + public class GenericDevelopmentEnvironment : IGeneratorEnvironment { + public string Name { get; } + + public GenericDevelopmentEnvironment() + { + Name = "Generic"; + } + public bool AttachDebugger(int processId) { return Debugger.Launch(); diff --git a/src/SourceGenerator.Foundations.Contracts/IDevelopmentPlatform.cs b/src/SourceGenerator.Foundations.Contracts/IGeneratorEnvironment.cs similarity index 61% rename from src/SourceGenerator.Foundations.Contracts/IDevelopmentPlatform.cs rename to src/SourceGenerator.Foundations.Contracts/IGeneratorEnvironment.cs index 3189259..d53460a 100644 --- a/src/SourceGenerator.Foundations.Contracts/IDevelopmentPlatform.cs +++ b/src/SourceGenerator.Foundations.Contracts/IGeneratorEnvironment.cs @@ -4,10 +4,16 @@ namespace SGF { /// - /// Abstracts for development environments supported by the platform + /// Contains information about the enviroment that this source generator is running in. It allows you to + /// star tthe debuggger and get the custom platform loggers based on the context. /// - public interface IDevelopmentPlatform + public interface IGeneratorEnvironment { + /// + /// Gets the name of the environment + /// + string Name { get; } + /// /// Attaches the debugger to the given process Id and returns back if it was successful or not. This can /// fail if Visual Studio is not already running diff --git a/src/SourceGenerator.Foundations.Contracts/IncrementalGenerator.cs b/src/SourceGenerator.Foundations.Contracts/IncrementalGenerator.cs new file mode 100644 index 0000000..48220bc --- /dev/null +++ b/src/SourceGenerator.Foundations.Contracts/IncrementalGenerator.cs @@ -0,0 +1,90 @@ +using System; +using System.Diagnostics; +using SGF.Diagnostics; +using Microsoft.CodeAnalysis; + +namespace SGF +{ + /// + /// Used as a base class for creating your own source generator. This class provides some helper + /// methods and impoved debugging expereince. The generator that implements this must apply the + /// but not inheirt from + /// + public abstract class IncrementalGenerator : IDisposable + { + private readonly IGeneratorEnvironment m_developmentPlatform; + + /// + /// Gets the name of the source generator + /// + public string Name { get; } + + /// + /// Gets the log that can allow you to output information to your + /// IDE of choice + /// + public ILogger Logger { get; } + + + /// + /// Initializes a new instance of the incremental generator with an optional name + /// + protected IncrementalGenerator( + string? name, + IGeneratorEnvironment developmentPlatform, + ILogger logger) + { + m_developmentPlatform = developmentPlatform; + Name = name ?? GetType().Name; + Logger = logger; + } + + /// + /// Implement to initalize the incremental source generator + /// + public abstract void OnInitialize(SgfInitializationContext context); + + /// + /// Override to add logic for disposing this instance and free resources + /// + protected virtual void Dipose() + { } + + /// + /// Attaches the debugger automtically if you are running from Visual Studio. You have the option + /// to stop or just continue + /// + protected void AttachDebugger() + { + Process process = Process.GetCurrentProcess(); + m_developmentPlatform.AttachDebugger(process.Id); + } + + /// + /// Raised when one of the generator functions throws an unhandle exception. Override this to define your own behaviour + /// to handle the exception. + /// + /// The exception that was thrown + protected virtual void OnException(Exception exception) + { + Logger.Error(exception, $"Unhandled exception was throw while running the generator {Name}"); + } + + /// + /// Events raised when the exception is being thrown by the app domain + /// + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception exception) + { + OnException(exception); + } + } + + /// + void IDisposable.Dispose() + { + Dipose(); + } + } +} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.Contracts/SgfGeneratorAttribute.cs b/src/SourceGenerator.Foundations.Contracts/SgfGeneratorAttribute.cs new file mode 100644 index 0000000..f74a3df --- /dev/null +++ b/src/SourceGenerator.Foundations.Contracts/SgfGeneratorAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace SGF +{ + /// + /// Applied a class the inheirts from + /// that will have Source Generator Foundations wrapper generated around it. This adds + /// better error handling and logging to the given generator. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class SgfGeneratorAttribute : Attribute + { + } +} diff --git a/src/SourceGenerator.Foundations.Contracts/SgfInitializationContext.cs b/src/SourceGenerator.Foundations.Contracts/SgfInitializationContext.cs new file mode 100644 index 0000000..f5b3e2b --- /dev/null +++ b/src/SourceGenerator.Foundations.Contracts/SgfInitializationContext.cs @@ -0,0 +1,131 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using SGF.Diagnostics; +using System; +using System.Reflection; + +namespace SGF +{ + /// + /// Middleware wrapper around a to allow for + /// wraping with exception handling and provide a better user expereince + /// + public readonly struct SgfInitializationContext + { + private readonly ILogger m_logger; + private readonly IncrementalGeneratorInitializationContext m_context; + + public SyntaxValueProvider SyntaxProvider => m_context.SyntaxProvider; + public IncrementalValueProvider CompilationProvider => m_context.CompilationProvider; + public IncrementalValueProvider ParseOptionsProvider => m_context.ParseOptionsProvider; + public IncrementalValuesProvider AdditionalTextsProvider => m_context.AdditionalTextsProvider; + public IncrementalValueProvider AnalyzerConfigOptionsProvider => m_context.AnalyzerConfigOptionsProvider; + public IncrementalValuesProvider MetadataReferencesProvider => m_context.MetadataReferencesProvider; + + public SgfInitializationContext( + IncrementalGeneratorInitializationContext context, + ILogger logger) + { + m_logger = logger; + m_context = context; + } + + public void RegisterSourceOutput(IncrementalValueProvider source, Action action) + { + ILogger logger = m_logger; + + void wrappedAction(SourceProductionContext context, TSource source) + { + try + { + action(new(context, logger), source); + } + catch (Exception exception) + { + LogException(logger, exception, action.Method); + } + } + m_context.RegisterSourceOutput(source, wrappedAction); + } + + public void RegisterSourceOutput(IncrementalValuesProvider source, Action action) + { + ILogger logger = m_logger; + void wrappedAction(SourceProductionContext context, TSource source) + { + try + { + action(new(context, logger), source); + } + catch (Exception exception) + { + LogException(logger, exception, action.Method); + } + } + m_context.RegisterSourceOutput(source, wrappedAction); + } + + public void RegisterImplementationSourceOutput(IncrementalValueProvider source, Action action) + { + ILogger logger = m_logger; + void wrappedAction(SourceProductionContext context, TSource source) + { + try + { + SgfSourceProductionContext sgfContext = new SgfSourceProductionContext(context, logger); + action(sgfContext, source); + logger.Information($" SourceFiles: {sgfContext.SourceCount}"); + } + catch (Exception exception) + { + LogException(logger, exception, action.Method); + } + } + m_context.RegisterImplementationSourceOutput(source, wrappedAction); + } + + + + public void RegisterImplementationSourceOutput(IncrementalValuesProvider source, Action action) + { + ILogger logger = m_logger; + + void wrappedAction(SourceProductionContext context, TSource source) + { + try + { + action(new(context, logger), source); + } + catch (Exception exception) + { + LogException(logger, exception, action.Method); + } + } + m_context.RegisterImplementationSourceOutput(source, wrappedAction); + } + + public void RegisterPostInitializationOutput(Action callback) + { + ILogger logger = m_logger; + void wrappedCallback(IncrementalGeneratorPostInitializationContext context) + { + try + { + callback(context); + } + catch (Exception exception) + { + LogException(logger, exception, callback.Method); + } + } + m_context.RegisterPostInitializationOutput(wrappedCallback); + } + + private static void LogException(ILogger logger, Exception exception, MethodInfo actionInfo) + { + string methodName = actionInfo.Name; + string className = actionInfo.DeclaringType.FullName; + logger.Error(exception, $"An {exception.GetType().Name} exception was thrown while invoking {className}.{methodName}"); + } + } +} diff --git a/src/SourceGenerator.Foundations.Contracts/SgfSourceProductionContext.cs b/src/SourceGenerator.Foundations.Contracts/SgfSourceProductionContext.cs new file mode 100644 index 0000000..b1310f0 --- /dev/null +++ b/src/SourceGenerator.Foundations.Contracts/SgfSourceProductionContext.cs @@ -0,0 +1,66 @@ +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; +using SGF.Diagnostics; +using System.Text; +using System.Threading; +using System.Reflection; +using System; + +namespace SGF +{ + /// + /// Wrapper around a used to help capture errors and report logs + /// + public struct SgfSourceProductionContext + { + private readonly ILogger m_logger; + private readonly SourceProductionContext m_context; + + /// + /// Gets the number of source files that were added + /// + public int SourceCount { get; private set; } + + + /// + /// A token that will be cancelled when generation should stop + /// + public CancellationToken CancellationToken => m_context.CancellationToken; + + internal SgfSourceProductionContext(SourceProductionContext context, ILogger logger) + { + SourceCount = 0; + m_logger = logger; + m_context = context; + + } + + /// + /// Adds source code in the form of a to the compilation. + /// + /// An identifier that can be used to reference this source text, must be unique within this generator + /// The source code to add to the compilation + public void AddSource(string hintName, string source) => AddSource(hintName, SourceText.From(source, Encoding.UTF8)); + + /// + /// Adds a to the compilation + /// + /// An identifier that can be used to reference this source text, must be unique within this generator + /// The to add to the compilation + public void AddSource(string hintName, SourceText sourceText) + { + SourceCount++; + m_logger.Information($" SourceAdded: {SourceCount}. {hintName}"); + m_context.AddSource(hintName, sourceText); + } + + /// + /// Adds a to the users compilation + /// + /// The diagnostic that should be added to the compilation + /// + /// The severity of the diagnostic may cause the compilation to fail, depending on the settings. + /// + public void ReportDiagnostic(Diagnostic diagnostic) => m_context.ReportDiagnostic(diagnostic); + } +} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.Contracts/SourceGenerator.Foundations.Contracts.csproj b/src/SourceGenerator.Foundations.Contracts/SourceGenerator.Foundations.Contracts.csproj index 51c86aa..1ffc593 100644 --- a/src/SourceGenerator.Foundations.Contracts/SourceGenerator.Foundations.Contracts.csproj +++ b/src/SourceGenerator.Foundations.Contracts/SourceGenerator.Foundations.Contracts.csproj @@ -9,4 +9,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + sgf/src/%(RecursiveDir)%(Filename)%(Extension) + True + + + sgf/src/%(RecursiveDir)%(Filename)%(Extension) + True + + diff --git a/src/SourceGenerator.Foundations.Injector/ErrorFactory.cs b/src/SourceGenerator.Foundations.Injector/ErrorFactory.cs deleted file mode 100644 index 745b658..0000000 --- a/src/SourceGenerator.Foundations.Injector/ErrorFactory.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace SourceGenerator.Foundations.Injector -{ - internal static class ExceptionFactory - { - internal static InjectionException AssemblyDoesNotExist(string assembly) - { - return new InjectionException($"Assembly '{assembly}' does not exist"); - } - - internal static InjectionException NoModuleInitializerTypeFound() - { - return new InjectionException("Found no type named 'ModuleInitializer', this type must exist or the ModuleInitializer parameter must be used"); - } - - internal static InjectionException InvalidFormatForModuleInitializer() - { - return new InjectionException("Invalid format for ModuleInitializer parameter, use Full.Type.Name::MethodName"); - } - - internal static InjectionException TypeNameDoesNotExist(string typeName) - { - return new InjectionException($"No type named '{typeName}' exists in the given assembly!"); - } - - internal static InjectionException MethodNameDoesNotExist(string typeName, string methodName) - { - return new InjectionException($"No method named '{methodName}' exists in the type '{methodName}'"); - } - - internal static InjectionException ModuleInitializerMayNotBePrivate() - { - return new InjectionException("Module initializer method may not be private or protected, use public or internal instead"); - } - - internal static InjectionException ModuleInitializerMustBeVoid() - { - return new InjectionException("Module initializer method must have 'void' as return new InjectionException(type"); - } - - internal static InjectionException ModuleInitializerMayNotHaveParameters() - { - return new InjectionException("Module initializer method must not have any parameters"); - } - - internal static InjectionException ModuleInitializerMustBeStatic() - { - return new InjectionException("Module initializer method must be static"); - } - - internal static Exception ModuleNotFound() - { - return new InjectionException("Unable to find main module in assembly"); - } - } -} diff --git a/src/SourceGenerator.Foundations.Injector/InjectionException.cs b/src/SourceGenerator.Foundations.Injector/InjectionException.cs deleted file mode 100644 index 2d35be0..0000000 --- a/src/SourceGenerator.Foundations.Injector/InjectionException.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SourceGenerator.Foundations.Injector -{ - public class InjectionException : Exception - { - public InjectionException(string msg) : base(msg) { } - } -} diff --git a/src/SourceGenerator.Foundations.Injector/Injector.cs b/src/SourceGenerator.Foundations.Injector/Injector.cs deleted file mode 100644 index e566e77..0000000 --- a/src/SourceGenerator.Foundations.Injector/Injector.cs +++ /dev/null @@ -1,145 +0,0 @@ -using Mono.Cecil; -using Mono.Cecil.Cil; -using Mono.Cecil.Pdb; -using Mono.Collections.Generic; -using System.Diagnostics; -using AssemblyDefinition = Mono.Cecil.AssemblyDefinition; - -namespace SourceGenerator.Foundations.Injector -{ - internal class Injector - { - - private string? PdbFile(string assemblyFile) - { - Debug.Assert(assemblyFile != null); - string path = Path.ChangeExtension(assemblyFile, ".pdb"); - return File.Exists(path) ? path : null; - } - - public void Inject(string assemblyFile, string typeName, string methodName) - { - AssemblyDefinition? assembly = null; - - try - { - if (!File.Exists(assemblyFile)) - { - throw ExceptionFactory.AssemblyDoesNotExist(assemblyFile); - } - assembly = ReadAssembly(assemblyFile); - MethodReference callee = GetCalleeMethod(typeName, methodName, assembly); - InjectInitializer(callee, assembly); - - WriteAssembly(assemblyFile, assembly); - } - catch (Exception ex) - { - throw new InjectionException(ex.Message); - } - finally - { - assembly?.Dispose(); - } - } - - private void InjectInitializer(MethodReference callee, AssemblyDefinition assemblyDefinition) - { - Debug.Assert(assemblyDefinition != null); - TypeReference voidRef = assemblyDefinition.MainModule.ImportReference(callee.ReturnType); - const MethodAttributes attributes = MethodAttributes.Static - | MethodAttributes.SpecialName - | MethodAttributes.RTSpecialName; - MethodDefinition cctor = new(".cctor", attributes, voidRef); - ILProcessor il = cctor.Body.GetILProcessor(); - il.Append(il.Create(OpCodes.Call, callee)); - il.Append(il.Create(OpCodes.Ret)); - - TypeDefinition? moduleClass = Find(assemblyDefinition.MainModule.Types, t => t.Name == ""); - if(moduleClass == null) - { - throw ExceptionFactory.ModuleNotFound(); - } - - // Always insert first so that we appear before [ModuleInitalizerAttribute] - moduleClass.Methods - .Insert(0, cctor); - - Debug.Assert(moduleClass != null, "Found no module class!"); - } - - private void WriteAssembly(string assemblyFile, AssemblyDefinition assembly) - { - var writeParams = new WriterParameters(); - if (PdbFile(assemblyFile) != null) - { - writeParams.WriteSymbols = true; - writeParams.SymbolWriterProvider = new PdbWriterProvider(); - } - assembly.Write(assemblyFile, writeParams); - } - - private AssemblyDefinition ReadAssembly(string assemblyFile) - { - var resolver = new DefaultAssemblyResolver(); - resolver.AddSearchDirectory(Path.GetDirectoryName(assemblyFile)); - - var readParams = new ReaderParameters(ReadingMode.Immediate) - { - AssemblyResolver = resolver, - InMemory = true - }; - - if (PdbFile(assemblyFile) != null) - { - readParams.ReadSymbols = true; - readParams.SymbolReaderProvider = new PdbReaderProvider(); - } - return AssemblyDefinition.ReadAssembly(assemblyFile, readParams); - } - - private MethodReference GetCalleeMethod(string typeName, string methodName, AssemblyDefinition assembly) - { - ModuleDefinition module = assembly.MainModule; - TypeDefinition? moduleInitializerClass; - - moduleInitializerClass = Find(module.Types, t => t.FullName == typeName); - if (moduleInitializerClass == null) - { - throw ExceptionFactory.TypeNameDoesNotExist(typeName); - } - - MethodDefinition? callee = Find(moduleInitializerClass.Methods, m => m.Name == methodName); - if (callee == null) - { - throw ExceptionFactory.MethodNameDoesNotExist(moduleInitializerClass.FullName, methodName); - } - if (callee.Parameters.Count > 0) - { - throw ExceptionFactory.ModuleInitializerMayNotHaveParameters(); - } - if (callee.IsPrivate || callee.IsFamily) - { - throw ExceptionFactory.ModuleInitializerMayNotBePrivate(); - } - if (!callee.ReturnType.FullName.Equals(typeof(void).FullName)) //Don't compare the types themselves, they might be from different CLR versions. - { - throw ExceptionFactory.ModuleInitializerMustBeVoid(); - } - return !callee.IsStatic ? throw ExceptionFactory.ModuleInitializerMustBeStatic() : callee; - } - - //No LINQ, since we want to target 2.0 - private static T? Find(Collection objects, Predicate condition) where T : class - { - foreach (T obj in objects) - { - if (condition(obj)) - { - return obj; - } - } - return null; - } - } -} diff --git a/src/SourceGenerator.Foundations.Injector/Program.cs b/src/SourceGenerator.Foundations.Injector/Program.cs deleted file mode 100644 index a33689f..0000000 --- a/src/SourceGenerator.Foundations.Injector/Program.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.CommandLine; -using System.CommandLine.Builder; -using System.CommandLine.Parsing; -using System.Runtime.CompilerServices; - -namespace SourceGenerator.Foundations.Injector -{ - internal class InjectCommand : RootCommand - { - - public InjectCommand() - { - Description = "Takes an assembly and injects a series of methods into the module initalizer that will be called on startup"; - - Argument className = new("className", "The full type name of the class that should be added to the moduel initializer"); - Argument methodName = new("methodName", "The name of the static method that should be added to the module initializer"); - Argument targetAssembly = new Argument("targetAssembly", "The path to the assembly file that should be injected"); - targetAssembly.ExistingOnly(); - - AddArgument(className); - AddArgument(methodName); - AddArgument(targetAssembly); - - this.SetHandler(InvokeAsync, className, methodName, targetAssembly); - } - - private Task InvokeAsync(string className, string methodName, FileInfo targetAssembly) - { - Injector injector = new Injector(); - injector.Inject(targetAssembly.FullName, className, methodName); - return Task.FromResult(0); - } - } - - internal class Program - { - static async Task Main(string[] args) - { - return await new CommandLineBuilder(new InjectCommand()) - .UseHelp() - .Build() - .InvokeAsync(args); - } - } -} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.Injector/Properties/launchSettings.json b/src/SourceGenerator.Foundations.Injector/Properties/launchSettings.json deleted file mode 100644 index 9235317..0000000 --- a/src/SourceGenerator.Foundations.Injector/Properties/launchSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "profiles": { - "SourceGenerator.Foundations.Injector": { - "commandName": "Project", - "commandLineArgs": "SGF.Reflection.AssemblyResolver Initialize D:\\Repositories\\SourceGenerator.Foudations\\src\\Sandbox\\ConsoleApp.SourceGenerator\\bin\\Debug\\netstandard2.0\\ConsoleApp.SourceGenerator.dll" - } - } -} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.Injector/SourceGenerator.Foundations.Injector.csproj b/src/SourceGenerator.Foundations.Injector/SourceGenerator.Foundations.Injector.csproj deleted file mode 100644 index b217a5c..0000000 --- a/src/SourceGenerator.Foundations.Injector/SourceGenerator.Foundations.Injector.csproj +++ /dev/null @@ -1,11 +0,0 @@ - - - Exe - net6.0 - enable - - - - - - diff --git a/src/SourceGenerator.Foundations.Shared/SourceGenerator.Foundations.Shared.projitems b/src/SourceGenerator.Foundations.Shared/SourceGenerator.Foundations.Shared.projitems index f1d8d78..2c63094 100644 --- a/src/SourceGenerator.Foundations.Shared/SourceGenerator.Foundations.Shared.projitems +++ b/src/SourceGenerator.Foundations.Shared/SourceGenerator.Foundations.Shared.projitems @@ -9,6 +9,6 @@ SGF - + \ No newline at end of file diff --git a/src/SourceGenerator.Foundations.sln b/src/SourceGenerator.Foundations.sln index 19e58e4..1f73b58 100644 --- a/src/SourceGenerator.Foundations.sln +++ b/src/SourceGenerator.Foundations.sln @@ -21,14 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGenerator.Foundations EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sandbox", "Sandbox", "{6118BF32-23BA-4D33-946E-F7E8A6F5D758}" EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SourceGenerator.Foundations.Shared", "SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.shproj", "{8AF3630C-2BF5-4854-A45D-0074C2787964}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "Sandbox\ConsoleApp\ConsoleApp.csproj", "{F4BA95B9-0353-44CA-9502-C74B532321B7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp.SourceGenerator", "Sandbox\ConsoleApp.SourceGenerator\ConsoleApp.SourceGenerator.csproj", "{594AACB5-B550-46CF-B6E2-16EF826D655A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGenerator.Foundations.Injector", "SourceGenerator.Foundations.Injector\SourceGenerator.Foundations.Injector.csproj", "{E9B785CC-B19E-499F-915D-7E37F727C50C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGenerator.Foundations.MSBuild", "SourceGenerator.Foundations.MSBuild\SourceGenerator.Foundations.MSBuild.csproj", "{E95587D6-78E8-48FE-9F98-371800A77B69}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{F0124C8F-B8FB-4832-9D65-D0F13E4308AE}" @@ -81,14 +77,6 @@ Global {594AACB5-B550-46CF-B6E2-16EF826D655A}.Release|Any CPU.Build.0 = Release|Any CPU {594AACB5-B550-46CF-B6E2-16EF826D655A}.Release|x64.ActiveCfg = Release|x64 {594AACB5-B550-46CF-B6E2-16EF826D655A}.Release|x64.Build.0 = Release|x64 - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|x64.ActiveCfg = Debug|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Debug|x64.Build.0 = Debug|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|Any CPU.Build.0 = Release|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|x64.ActiveCfg = Release|Any CPU - {E9B785CC-B19E-499F-915D-7E37F727C50C}.Release|x64.Build.0 = Release|Any CPU {E95587D6-78E8-48FE-9F98-371800A77B69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E95587D6-78E8-48FE-9F98-371800A77B69}.Debug|Any CPU.Build.0 = Debug|Any CPU {E95587D6-78E8-48FE-9F98-371800A77B69}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -109,10 +97,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EDB10920-970A-43F9-A2B3-7F1270DD477B} EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.projitems*{0085a518-02b6-4218-b718-00c0f69b99ab}*SharedItemsImports = 5 - SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.projitems*{594aacb5-b550-46cf-b6e2-16ef826d655a}*SharedItemsImports = 5 - SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.projitems*{8af3630c-2bf5-4854-a45d-0074c2787964}*SharedItemsImports = 13 - SourceGenerator.Foundations.Shared\SourceGenerator.Foundations.Shared.projitems*{dc8c5a1a-6269-4ba7-852a-d21cb0b2b5a0}*SharedItemsImports = 5 - EndGlobalSection EndGlobal diff --git a/src/SourceGenerator.Foundations/HoistSourceGenerator.cs b/src/SourceGenerator.Foundations/HoistSourceGenerator.cs new file mode 100644 index 0000000..3261bb8 --- /dev/null +++ b/src/SourceGenerator.Foundations/HoistSourceGenerator.cs @@ -0,0 +1,87 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using SGF.Models; +using SGF.Templates; +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace SGF +{ + + /// + /// Most basic generator used to generate base class for all the generator + /// classes defined within the project + /// + [Generator] + internal class HoistSourceGenerator : IIncrementalGenerator + { + public void Initialize(IncrementalGeneratorInitializationContext context) + { + + var provider = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsSyntaxTargetForGeneration(s), + transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) + .Where(static m => m is not null); + + context.RegisterSourceOutput(context.AnalyzerConfigOptionsProvider, AddCoreTypes); + context.RegisterSourceOutput(provider, + static (spc, source) => Execute(source!, spc)); + } + + private void AddCoreTypes(SourceProductionContext context, AnalyzerConfigOptionsProvider optionsProvider) + { + string @namespace = "SGF"; + + context.AddSource("SgfSourceGeneratorHoist.g.cs", SourceGeneratorHoistBase.RenderTemplate(@namespace)); + context.AddSource("SgfAssemblyResolver.g.cs", AssemblyResolverTemplate.Render(@namespace)); + + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode s) + => s is ClassDeclarationSyntax classDeclarationSyntax && + classDeclarationSyntax.AttributeLists.Count > 0; + + private static SourceGeneratorDataModel? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) + { + // we know the node is a ClassDeclarationSyntax thanks to IsSyntaxTargetForGeneration + ClassDeclarationSyntax classDeclrationSyntax = (ClassDeclarationSyntax)context.Node; + + // loop through all the attributes on the method + foreach (AttributeListSyntax attributeListSyntax in classDeclrationSyntax.AttributeLists) + { + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + if (context.SemanticModel.GetSymbolInfo(attributeSyntax).Symbol is not IMethodSymbol attributeSymbol) + { + // weird, we couldn't get the symbol, ignore it + continue; + } + + INamedTypeSymbol attributeContainingTypeSymbol = attributeSymbol.ContainingType; + string fullName = attributeContainingTypeSymbol.ToDisplayString(); + + // Is the attribute the [EnumExtensions] attribute? + if (fullName == "SGF.SgfGeneratorAttribute") + { + // return the enum. Implementation shown in section 7. + return SourceGeneratorDataModel.Create(classDeclrationSyntax, context.SemanticModel); + } + } + } + + return null; + } + + private static void Execute(SourceGeneratorDataModel dataModel, SourceProductionContext context) + { + SourceText classDefinition = SourceGeneratorHostImpl.RenderTemplate(dataModel); + context.AddSource($"Sgf{dataModel.ClassName}Host.g.cs", classDefinition); + } + } +} diff --git a/src/SourceGenerator.Foundations/Models/SourceGeneratorDataModel.cs b/src/SourceGenerator.Foundations/Models/SourceGeneratorDataModel.cs new file mode 100644 index 0000000..a2198be --- /dev/null +++ b/src/SourceGenerator.Foundations/Models/SourceGeneratorDataModel.cs @@ -0,0 +1,37 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SGF.Models +{ + internal class SourceGeneratorDataModel + { + public string ClassName { get; } + public string Namespace { get; } + public string AccessModifier { get; } + public string QualifedName { get; } + + public SourceGeneratorDataModel(string name, string @namespace, string accessModifier) + { + ClassName = name; + Namespace = @namespace; + AccessModifier = accessModifier; + QualifedName = string.IsNullOrEmpty(Namespace) ? ClassName : $"{Namespace}.{ClassName}"; + } + + public static SourceGeneratorDataModel? Create(ClassDeclarationSyntax classDeclarationSyntax, SemanticModel semanticModel) + { + INamedTypeSymbol? typeSymbol = semanticModel.GetDeclaredSymbol(classDeclarationSyntax); + if (typeSymbol == null) + { + return null; + } + + return new SourceGeneratorDataModel( + name: typeSymbol.Name, + @namespace: typeSymbol.ContainingNamespace.ToDisplayString(), + accessModifier: typeSymbol.DeclaredAccessibility == Accessibility.Public ? "public" : "internal" + ); + } + } +} diff --git a/src/SourceGenerator.Foundations/Properties/launchSettings.json b/src/SourceGenerator.Foundations/Properties/launchSettings.json index cca2e10..0a77eb7 100644 --- a/src/SourceGenerator.Foundations/Properties/launchSettings.json +++ b/src/SourceGenerator.Foundations/Properties/launchSettings.json @@ -3,7 +3,6 @@ "ConsoleApp.SourceGenerator Target": { "commandName": "DebugRoslynComponent", "targetProject": "..\\..\\src\\Sandbox\\ConsoleApp.SourceGenerator\\ConsoleApp.SourceGenerator.csproj", - } } } \ No newline at end of file diff --git a/src/SourceGenerator.Foundations/SourceGenerator.Foundations.csproj b/src/SourceGenerator.Foundations/SourceGenerator.Foundations.csproj index e4b9de5..788be2b 100644 --- a/src/SourceGenerator.Foundations/SourceGenerator.Foundations.csproj +++ b/src/SourceGenerator.Foundations/SourceGenerator.Foundations.csproj @@ -4,6 +4,7 @@ netstandard2.0 true false + 11 true $(TargetsForTfmSpecificContentInPackage);CustomNugetPack @@ -19,19 +20,14 @@ - - TargetFramework=net6.0 - - - - + build/ - + \ No newline at end of file diff --git a/src/SourceGenerator.Foundations/SourceGenerator.Foundations.targets b/src/SourceGenerator.Foundations/SourceGenerator.Foundations.targets index bb16009..7d2b9ba 100644 --- a/src/SourceGenerator.Foundations/SourceGenerator.Foundations.targets +++ b/src/SourceGenerator.Foundations/SourceGenerator.Foundations.targets @@ -23,9 +23,9 @@ - + diff --git a/src/SourceGenerator.Foundations/Templates/AssemblyResolver.cs b/src/SourceGenerator.Foundations/Templates/AssemblyResolver.cs new file mode 100644 index 0000000..c44f53f --- /dev/null +++ b/src/SourceGenerator.Foundations/Templates/AssemblyResolver.cs @@ -0,0 +1,195 @@ +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace SGF.Templates; +public static class AssemblyResolverTemplate +{ + public static SourceText Render(string @namespace) => SourceText.From($$""" +#nullable enable +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace {{@namespace}} +{ + /// + /// Used to compare two to pull them out of the dictionary of types + /// + internal class AssemblyNameComparer : IEqualityComparer + { + public bool Equals(AssemblyName x, AssemblyName y) + { + return string.Equals(GetName(x), GetName(y)); + } + + public int GetHashCode(AssemblyName obj) + { + return GetName(obj).GetHashCode(); + } + + private static string GetName(AssemblyName assemblyName) + { + string name = assemblyName.FullName; + int index = name.IndexOf(','); + return index <= 0 + ? name + : name.Substring(0, index); + } + } + + internal static class AssemblyResolver + { + private static readonly List s_assembliesWithResources; + private static readonly Dictionary s_loadedAssemblies; + + static AssemblyResolver() + { + s_assembliesWithResources = new List(); + s_loadedAssemblies = new Dictionary(new AssemblyNameComparer()); + } + + internal static void Initialize() + { + // The assembly resolvers get added to multiple source generators + // so what we do here is only allow the first one defined to allow + // itself to be a resolver. Since this could lead to cases where two resolvers + // exists and provide two different instances of the same assembly. + const string RESOLVER_ATTACHED_KEY = "SGF_ASSEMBLY_RESOLVER_IS_ATTACHED"; + AppDomain currentDomain = AppDomain.CurrentDomain; + object? rawValue = currentDomain.GetData(RESOLVER_ATTACHED_KEY); + + if (rawValue == null || (rawValue is bool isAttached && !isAttached)) + { + currentDomain.SetData(RESOLVER_ATTACHED_KEY, true); + currentDomain.AssemblyLoad += OnAssemblyLoaded; + currentDomain.AssemblyResolve += ResolveMissingAssembly; + + foreach (Assembly assembly in currentDomain.GetAssemblies()) + { + AddAssembly(assembly); + } + } + } + + /// + /// Raised whenever our app domain loads a new assembly + /// + /// THe thing that raised the event + /// The parameters + private static void OnAssemblyLoaded(object sender, AssemblyLoadEventArgs args) + { + AddAssembly(args.LoadedAssembly); + } + + /// + /// Adds an assembly to the veriuos collections used to keep track of loaded items + /// + private static void AddAssembly(Assembly assembly) + { + AssemblyName assemblyName = assembly.GetName(); + + if (s_loadedAssemblies.ContainsKey(assemblyName)) + { + return; + } + s_loadedAssemblies.Add(assemblyName, assembly); + + if (assembly.IsDynamic) return; + + string[] resources = assembly.GetManifestResourceNames() + .Where(r => r.StartsWith("SGF.Assembly::")) + .ToArray(); + + if (resources.Length == 0) return; + + foreach (string resource in resources) + { + Console.WriteLine($"Extracting {resource} assembly from {assemblyName.Name}'s resources."); + if (TryExtractingAssembly(assembly, resource, out Assembly? loadedAssembly)) + { + AddAssembly(loadedAssembly!); + } + } + } + + /// + /// Attempts to resolve any assembly by looking for dependencies that are embedded directly + /// in this dll. + /// + private static Assembly? ResolveMissingAssembly(object sender, ResolveEventArgs args) + { + AssemblyName assemblyName = new(args.Name); + + if (s_loadedAssemblies.TryGetValue(assemblyName, out Assembly assembly)) + { + return assembly; + } + + foreach (Assembly loadedAssembly in s_assembliesWithResources) + { + string resourceName = $"SGF.Assembly::{assemblyName.Name}.dll"; + if (TryExtractingAssembly(loadedAssembly, resourceName, out Assembly? extractedAssembly)) + { + AddAssembly(extractedAssembly!); + return extractedAssembly!; + }; + } + + return null; + } + + + /// + /// Attempts to load an assembly that is contained within aonther assembly as a resource + /// + /// The assembly that should contain the resource + /// The expected name of the reosurce + /// The assembly if it was loaded + /// True if the assembly could be loaded otherwise false + private static bool TryExtractingAssembly(Assembly assembly, string resourceName, out Assembly? loadedAssembly) + { + loadedAssembly = null; + if (TryGetResourceBytes(assembly, resourceName, out byte[]? assemblyBytes)) + { +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + loadedAssembly = TryGetResourceBytes(assembly, Path.ChangeExtension(resourceName, ".pdb"), out byte[]? symbolBytes) + ? Assembly.Load(assemblyBytes, symbolBytes) + : Assembly.Load(assemblyBytes); +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + return true; + } + return false; + } + + /// + /// Attempts to read bytes from a resource and returns back if it's successful or not + /// + /// The assembly to pull the resource from + /// The name of the resource + /// The bytes[] if the resource could be found + /// True if the resource was found otherwise false + private static bool TryGetResourceBytes(Assembly assembly, string resourceName, out byte[]? bytes) + { + bytes = null; + ManifestResourceInfo resourceInfo = assembly.GetManifestResourceInfo(resourceName); + if (resourceInfo == null) + { + return false; + } + + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + { + bytes = new byte[stream.Length]; + _ = stream.Read(bytes, 0, bytes.Length); + } + + return true; + } + } +} +""", Encoding.UTF8); +} \ No newline at end of file diff --git a/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs new file mode 100644 index 0000000..9f33f03 --- /dev/null +++ b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHoistBase.cs @@ -0,0 +1,136 @@ + +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace SGF.Templates; + +public static class SourceGeneratorHoistBase +{ + public static SourceText RenderTemplate(string @namespace) => SourceText.From($$""" +#nullable enable +using System; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using SGF.Environments; +using SGF.Diagnostics; +using SGF.Diagnostics.Sinks; + +namespace {{@namespace}} +{ + /// + /// Provides logic for hooking into the app domain to resolve assemblies as well + // as capture exceptions and handle shutdown events + /// + internal abstract class SourceGeneratorHoist + { + protected static readonly AppDomain s_currentDomain; + + protected readonly ILogger m_logger; + // Needs to be an 'object' otherwise the assemblies will be attempted go be loaded before our assembly resolver + protected readonly object m_environment; + + /// + /// Gets the name of the source generator for logging purposes + /// + public string Name { get; } + + static SourceGeneratorHoist() + { + AssemblyResolver.Initialize(); + s_currentDomain = AppDomain.CurrentDomain; + } + + protected SourceGeneratorHoist(string name) + { + Name = name; + IGeneratorEnvironment environment = GetEnvironment(); + m_environment = environment; + s_currentDomain.ProcessExit += OnProcessExit; + + m_logger = new Logger(Name); + if(environment != null) + { + foreach(ILogSink logSink in environment.GetLogSinks()) + { + m_logger.AddSink(logSink); + } + } + + if (Environment.UserInteractive) + { + m_logger.AddSink(); + } + } + + protected virtual void Dispose() + { + s_currentDomain.ProcessExit -= OnProcessExit; + } + + /// + /// Raised when the process is closing, giving us a chance to cleanup any resources + /// + private void OnProcessExit(object sender, EventArgs e) + { + try + { + Dispose(); + } + catch (Exception ex) + { + m_logger.Error(ex, $"Exception thrown while disposing '{Name}'"); + } + } + + /// + /// Gets an instance of a development platform to be used to log and debug info + /// + /// + private static IGeneratorEnvironment GetEnvironment() + { + string? platformAssembly = null; + IGeneratorEnvironment? platform = null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Windows Development Platform + platformAssembly = "SourceGenerator.Foundations.Windows"; + } + else + { + // Generic Development Platform + platformAssembly = "SourceGenerator.Foundations.Contracts"; + } + + if (!string.IsNullOrWhiteSpace(platformAssembly)) + { + AssemblyName assemblyName = new(platformAssembly); + Assembly? assembly = null; + try + { + assembly = Assembly.Load(platformAssembly); + Type? platformType = assembly? + .GetTypes() + .Where(typeof(IGeneratorEnvironment).IsAssignableFrom) + .FirstOrDefault(); + if (platformType != null) + { + platform = Activator.CreateInstance(platformType) as IGeneratorEnvironment; + } + } + catch + { } + } + + return platform ?? new GenericDevelopmentEnvironment(); + } + } +} +""", Encoding.UTF8); +} + + + diff --git a/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs new file mode 100644 index 0000000..bd723d1 --- /dev/null +++ b/src/SourceGenerator.Foundations/Templates/SourceGeneratorHostImpl.cs @@ -0,0 +1,91 @@ +using Microsoft.CodeAnalysis.Text; +using SGF.Models; +using System.Text; + +namespace SGF.Templates; + +internal static class SourceGeneratorHostImpl +{ + public static SourceText RenderTemplate(SourceGeneratorDataModel dataModel) => SourceText.From($$""" +#nullable enable +using SGF; +using SGF.Diagnostics; +using System; +using System.Linq; +using System.Reflection; +using System.Diagnostics; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using System.Text; + +namespace {{dataModel.Namespace}} +{ + [global::Microsoft.CodeAnalysis.GeneratorAttribute] + internal class {{dataModel.ClassName}}Hoist : SourceGeneratorHoist, IIncrementalGenerator + { + // Has to be untyped otherwise it will try to resolve at startup + private object? m_generator; + + public {{dataModel.ClassName}}Hoist() : base("{{dataModel.ClassName}}") + { + m_generator = null; + } + + /// + /// Initializes the source generator to make it simpler to work with + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // The expected arguments types for the generator being created + Type[] typeArguments = new Type[] + { + typeof(IGeneratorEnvironment), + typeof(ILogger), + }; + + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + Type generatorType = typeof(global::{{dataModel.QualifedName}}); + ConstructorInfo? constructor = generatorType.GetConstructor(bindingFlags, null, typeArguments, Array.Empty()); + + if(constructor == null) + { + string argumentString = string.Join(", ", typeArguments.Select(t => t.Name)); + m_logger.Error($"Unable to create instance of {generatorType.FullName} as no constructor could be found that takes {argumentString}. The generator will not be run"); + return; + } + + + object[] constructorArguments = new object[] + { + m_environment, + m_logger + }; + IncrementalGenerator generator = (global::{{dataModel.QualifedName}})constructor.Invoke(constructorArguments); + m_generator = generator; + try + { + SgfInitializationContext sgfContext = new(context, m_logger); + + generator.OnInitialize(sgfContext); + } + catch (Exception exception) + { + m_logger.Error(exception, $"Error! An unhandle exception was thrown while initializing the source generator '{Name}'."); + } + } + + protected override void Dispose() + { + base.Dispose(); + if(m_generator is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} +""", Encoding.UTF8); +} + + + From 544268c44205011c1c91c6facaaad1969fc3a3fb Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Fri, 16 Feb 2024 18:44:28 -0500 Subject: [PATCH 2/4] Source generators now write to the output window --- .../VisualStudio/VisualStudioLogEventSink.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Plugins/SourceGenerator.Foundations.Windows/Interop/VisualStudio/VisualStudioLogEventSink.cs b/src/Plugins/SourceGenerator.Foundations.Windows/Interop/VisualStudio/VisualStudioLogEventSink.cs index 32160fe..8dbc040 100644 --- a/src/Plugins/SourceGenerator.Foundations.Windows/Interop/VisualStudio/VisualStudioLogEventSink.cs +++ b/src/Plugins/SourceGenerator.Foundations.Windows/Interop/VisualStudio/VisualStudioLogEventSink.cs @@ -97,21 +97,21 @@ public void Write(LogLevel logLevel, string message) if (logLevel >= LogLevel.Warning) { - Write(message); + Write(message, m_buildOutput); } - Write(message); + Write(message, m_sourceGeneratorOutput); } /// /// Writes an entry to the output window if it has been initialized /// - public void Write(string message) + public void Write(string message, OutputWindowPane? outputPain) { try { - m_sourceGeneratorOutput?.OutputString(message); + outputPain?.OutputString(message); } catch (COMException) { @@ -122,11 +122,5 @@ public void Write(string message) Console.WriteLine($"Exception was thrown while writing log. Please report this on gihub {exception}"); } } - - /// - /// Writes an entry to the output window if it has been initialized - /// - public void WriteLine(string message) - => Write($"{message}{Environment.NewLine}"); } } \ No newline at end of file From ead13eda71164df117c3dcf01c8276a879471845 Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Fri, 16 Feb 2024 21:15:18 -0500 Subject: [PATCH 3/4] Fixed the ci/cd building the removed project --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5aa915..2a83fb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,6 @@ jobs: - name: GitVersion run: dotnet gitversion /output buildserver # Build Dependencies - - name: Build | SourceGenerator.Foundations.Injector - run: dotnet build src\SourceGenerator.Foundations.Injector\SourceGenerator.Foundations.Injector.csproj -p:Version=${{env.GitVersion_AssemblySemVer}} - name: Build | SourceGenerator.Foundations.MSBuild run: dotnet build src\SourceGenerator.Foundations.MSBuild\SourceGenerator.Foundations.MSBuild.csproj -p:Version=${{env.GitVersion_AssemblySemVer}} # Build Main From b7c7aecacdc093875c6bb0899765c93433bcc258 Mon Sep 17 00:00:00 2001 From: Byron Mayne Date: Fri, 16 Feb 2024 21:24:39 -0500 Subject: [PATCH 4/4] Updated the documentation about how to build it --- README.md | 24 ++++++++++++------- .../ConsoleAppSourceGenerator.cs | 2 +- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cbd2071..9ef1248 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,28 @@ Source Generators are awesome but working with them can be a bit painful. This l ## Quick Start -To get started all you need to do is add the NuGet package reference. You may or may not have to restart Visual Studio for the new types to show up. Then implement the base class `IncrementalGenerator`. +To get started all you need to do is add the NuGet package reference. You may or may not have to restart Visual Studio for the new types to show up. Then implement the base class `IncrementalGenerator` and apply the `[SgfGenerator]` attribute to your type; ```cs namespace Example { // IncrementalGenerator, is a generated type from `SourceGenerator.Foundations' + [SgfGenerator] public class ExampleSourceGenerator : IncrementalGenerator { + // Constructor can only take two arguments in this order + public ExampleSourceGenerator( + IGeneratorEnvironment generatorEnvironment, + ILogger logger) : base("ExampleSourceGenerator", + generatorPlatform, logger) + { + + } + // SgfInitializationContext is a wrapper around IncrementalGeneratorInitializationContext protected override void OnInitialize(SgfInitializationContext context) { - // Attaches Visaul Studio debugger without prompt + // Attaches Visual Studio debugger without prompt AttachDebugger(); // Writes output to Visual Studios output window @@ -27,6 +37,10 @@ namespace Example } ``` +> 🛈 You don't apply the `SourceGenerator` attribute or implement the `IIncrementalGenerator` interface. Behind the scenes Source Generator Foundations is generating a class that will host your generator. This is done so that we can capture all errors and also resolve any type loading. + + + # What is inside? There is a series of features this library hopes to provide. @@ -178,12 +192,6 @@ A very small library that only appears to contain a C# analyzer for adding error Contains common classes and utility methods that can be leveraged by source generators. -### Source.Generator.Foundations.Injector - -Uses `Mono.Cecil` to inject a static call to the `AssemblyResolver` to initialize as soon as the module is loaded. This same effect could be achieved using the `[ModuleInitialize]` attribute. However, this has two downsides. One, if other classes in the referencing project also use this attribute, the order of loading will not be known. Two, this attribute is not part of .netStandard so it has to be injected, there is a chance of conflict. - - - ### Source.Generator.Foundations.MSBuild Contains a custom MSBuild C# target implementation used to figure out which assemblies should be embedded as resources and which should be ignored. For example this will not embed any resources that are part of `.netstandard`. diff --git a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs index 5148d6e..493dabf 100644 --- a/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs +++ b/src/Sandbox/ConsoleApp.SourceGenerator/ConsoleAppSourceGenerator.cs @@ -15,7 +15,7 @@ public class Payload public string? Version { get; set; } } - public ConsoleAppSourceGenerator(IGeneratorEnvironment generatorPlatform, ILogger logger) : base("ConsoleAppSourceGenerator", generatorPlatform, logger) + public ConsoleAppSourceGenerator(IGeneratorEnvironment generatorEnvironment, ILogger logger) : base("ConsoleAppSourceGenerator", generatorEnvironment, logger) { }