Skip to content

Commit

Permalink
Merge branch 'feature/hoist-pattern'
Browse files Browse the repository at this point in the history
  • Loading branch information
ByronMayne committed Feb 21, 2024
2 parents 627f33c + 0009ca6 commit 51774e1
Show file tree
Hide file tree
Showing 30 changed files with 237 additions and 873 deletions.
50 changes: 1 addition & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Example
IGeneratorEnvironment generatorEnvironment,
ILogger logger) : base("ExampleSourceGenerator",
generatorPlatform, logger)
{
{$$

}

Expand Down Expand Up @@ -108,46 +108,6 @@ This will popup the following window and you have to select your visual studio i

![AutoAttach](./img/DebuggerAttach.gif)

## Polyfills

Source generators are written targeting `netstandard2.0` which means you don't have access to some features of the language. By using this library you will get the following.

### `init` Properties:
```cs
// init gives a compiler error without the polyfill
public string MyProperty { get; init; }
```
These can be disabled by setting the `<SgfAddInitPropertyPolyfill>false<SgfAddInitPropertyPolyfill>`. The default value is `true`.

### Required Members
A way of specifying that a property or field is required to be set during object initialization, forcing the instance creator to provide an initial value for the member in an object initializer at the creation site.

```cs
public class MyClass
{
public required string Name { get; init; }
}
```
These can be disabled by setting the `<SgfAddRequiredMemberPolyfill>false<SgfAddRequiredMemberPolyfill>`. The default value is `true`.

### Nullable Attributes
These attributes enable the compiler to provide warnings when you may dereference a null value, throwing a [System.NullReferenceException](https://learn.microsoft.com/en-us/dotnet/api/system.nullreferenceexception).
| Attribute | Category | Meaning |
|-----------|----------|---------|
| `AllowNull` | Precondition | A non-nullable parameter, field, or property may be null.
| `DisallowNull` | Precondition | A nullable parameter, field, or property should never be null.
| `MaybeNull` | Postcondition | A non-nullable parameter, field, property, or return value may be null.
| `NotNull` | Postcondition | A nullable parameter, field, property, or return value will never be null.
| `MaybeNullWhen` | Conditional postcondition | A non-nullable argument may be null when the method returns the specified bool value.
| `NotNullWhen` | Conditional postcondition | A nullable argument won't be null when the method returns the specified bool value.
| `NotNullIfNotNull` | Conditional postcondition | A return value, property, or argument isn't null if the argument for the specified parameter isn't null.
| `MemberNotNull` | Method and property helper methods | The listed member won't be null when the method returns.
| `MemberNotNullWhen` | Method and property helper methods | The listed member won't be null when the method returns the specified bool value.
| `DoesNotReturn` | Unreachable code | A method or property never returns. In other words, it always throws an exception.
| `DoesNotReturnIf` | Unreachable code | This method or property never returns if the associated bool parameter has the specified value.

These can be disabled by setting the `<SgfAddNullablePolyfill>false<SgfAddNullablePolyfill>`. The default value is `true`.

## Helpers

Included in the project is a series of helper classes to help you while working on your generator.
Expand All @@ -162,14 +122,6 @@ Included in the project is a series of helper classes to help you while working

# How it works

## Script Injector
It all starts with `ScriptInjector.cs`. This is a source generator loops over the resources within the current assembly and finds all `*.cs` files whos name starts with `SGF.Script::` and copies them into the target assembly. The image below shows
some of the scripts that are copied.

![ScriptInjector](./img/ScriptInjector.jpg)

If you would like a script to be copied over you can add it as a `ItemGroup` element called `SGF_EmbeddedScript` which will be embedded in your assembly and prefixed with the correct name.

## Assembly Resolver
When your source generator runs it needs to find it's dependencies and this is often what fails. When you want to provide external packages you will have to write your own resolve that is able to locate the required assemblies. So instead we have the `ScriptInjector` inject an implemention for you. This assembly resolver will loop over all the resources in the current assembly and look for all resources that start with `SGF.Assembly::`. If the assembly being requested to be loaded exists in the resources, it's extracted and loaded into the current appdomain.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SGFPropsPath)" />
<PropertyGroup>
<LangVersion>11</LangVersion>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<Platforms>AnyCPU;x64</Platforms>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Newtonsoft.Json;
using SGF;
using SGF.Diagnostics;
using System;
using System.Text;

namespace ConsoleApp.SourceGenerator
{
Expand All @@ -15,7 +17,7 @@ public class Payload
public string? Version { get; set; }
}

public ConsoleAppSourceGenerator(IGeneratorEnvironment generatorEnvironment, ILogger logger) : base("ConsoleAppSourceGenerator", generatorEnvironment, logger)
public ConsoleAppSourceGenerator() : base("ConsoleAppSourceGenerator")
{

}
Expand All @@ -33,9 +35,31 @@ public override void OnInitialize(SgfInitializationContext context)
Logger.Information("This generator references Newtonsoft.Json and it can just be referenced without any other boilerplate");
Logger.Information(JsonConvert.SerializeObject(payload));
Logger.Information("Having the log makes working with generators much simpler!");

context.RegisterPostInitializationOutput(AddSource);
}

private void AddSource(IncrementalGeneratorPostInitializationContext context)
{
SourceText sourceText = SourceText.From("""
namespace Examples
{
public class Person
{
public string Name { get; }

public Person(string name)
{
Name = name;
}
}
}
""", Encoding.UTF8);

context.AddSource("Person.g.cs", sourceText);
}

protected override void OnException(Exception exception)
public override void OnException(Exception exception)
{

base.OnException(exception);
Expand Down
5 changes: 4 additions & 1 deletion src/Sandbox/ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
public static class Program
using Examples;

public static class Program
{
public static void Main(string[] arguments)
{
Person person = new Person("");
//Testing(1);
}

Expand Down
86 changes: 74 additions & 12 deletions src/SourceGenerator.Foundations.Contracts/IncrementalGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
using System.Diagnostics;
using SGF.Diagnostics;
using Microsoft.CodeAnalysis;
using SGF.Environments;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Linq;
using SGF.Diagnostics.Sinks;

namespace SGF
{
Expand All @@ -12,31 +17,46 @@ namespace SGF
/// </summary>
public abstract class IncrementalGenerator : IDisposable
{
private readonly IGeneratorEnvironment m_developmentPlatform;
private readonly static IGeneratorEnvironment s_environment;

/// <summary>
/// Gets the name of the source generator
/// </summary>
public string Name { get; }
public string Name { get; private set; }

/// <summary>
/// Gets the log that can allow you to output information to your
/// IDE of choice
/// </summary>
public ILogger Logger { get; }
public ILogger Logger { get; private set; } // Set with reflection, don't change


static IncrementalGenerator()
{
s_environment = CreateEnvironment();
}

/// <summary>
/// Initializes a new instance of the incremental generator with an optional name
/// Initializes a new instance of the incremental generator. Note both <paramref name="developmentPlatform"/>
/// and <paramref name="logger"/> will be provided by the framework.
/// </summary>
protected IncrementalGenerator(
string? name,
IGeneratorEnvironment developmentPlatform,
ILogger logger)
protected IncrementalGenerator(string? name)
{
m_developmentPlatform = developmentPlatform;
Name = name ?? GetType().Name;
Logger = logger;
Logger = new Logger(Name);
if(s_environment != null)
{
foreach(ILogSink sink in s_environment.GetLogSinks())
{
Logger.AddSink(sink);
}
}
#pragma warning disable RS1035 // Do not use APIs banned for analyzers
if (Environment.UserInteractive)
{
Logger.AddSink<ConsoleSink>();
}
#pragma warning restore RS1035 // Do not use APIs banned for analyzers
}

/// <summary>
Expand All @@ -57,15 +77,15 @@ protected virtual void Dipose()
protected void AttachDebugger()
{
Process process = Process.GetCurrentProcess();
m_developmentPlatform.AttachDebugger(process.Id);
s_environment.AttachDebugger(process.Id);
}

/// <summary>
/// Raised when one of the generator functions throws an unhandle exception. Override this to define your own behaviour
/// to handle the exception.
/// </summary>
/// <param name="exception">The exception that was thrown</param>
protected virtual void OnException(Exception exception)
public virtual void OnException(Exception exception)
{
Logger.Error(exception, $"Unhandled exception was throw while running the generator {Name}");
}
Expand All @@ -81,6 +101,48 @@ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
}
}

/// <summary>
/// Gets an instance of a development platform to be used to log and debug info
/// </summary>
/// <returns></returns>
private static IGeneratorEnvironment CreateEnvironment()
{
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();
}

/// <inheritdoc cref="IDisposable"/>
void IDisposable.Dispose()
{
Expand Down
Loading

0 comments on commit 51774e1

Please sign in to comment.