feat: implemented SingletonComponent to wrap most of the Is* components

This commit is contained in:
D. Moonfire 2023-01-16 12:38:29 -06:00
parent a5694d0cee
commit e02c56e77e
21 changed files with 276 additions and 138 deletions

View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1667395993,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -17,11 +17,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1662019588, "lastModified": 1673631141,
"narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", "narHash": "sha256-AprpYQ5JvLS4wQG/ghm2UriZ9QZXvAwh1HlgA/6ZEVQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "2da64a81275b68fdad38af669afeda43d401e94b", "rev": "befc83905c965adfd33e5cae49acb0351f6e0404",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Calendar; namespace MfGames.Nitride.Calendar;
/// <summary> /// <summary>
/// A marker component for identifying an entity that represents a calendar. /// A marker component for identifying an entity that represents a calendar.
/// </summary> /// </summary>
public record IsCalendar [SingletonComponent]
public partial class IsCalendar
{ {
public static IsCalendar Instance { get; } = new();
} }

View file

@ -1,13 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds; namespace MfGames.Nitride.Feeds;
/// <summary> /// <summary>
/// A marker component that indicates this page is a feed. /// A marker component that indicates this page is a feed.
/// </summary> /// </summary>
public class IsFeed [SingletonComponent]
public partial class IsFeed
{ {
public IsFeed()
{
}
public static IsFeed Instance { get; } = new();
} }

View file

@ -1,10 +1,12 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Gemtext; namespace MfGames.Nitride.Gemtext;
/// <summary> /// <summary>
/// A marker component for indicating that an entity is Gemtext, the format /// A marker component for indicating that an entity is Gemtext, the format
/// for text files using the Gemini protocol. /// for text files using the Gemini protocol.
/// </summary> /// </summary>
public record IsGemtext [SingletonComponent]
public partial class IsGemtext
{ {
public static IsGemtext Instance { get; } = new();
} }

View file

@ -6,9 +6,9 @@ namespace MfGames.Nitride.Generators;
/// <summary> /// <summary>
/// Internal class that consolidates all of the information needed to generate a /// Internal class that consolidates all of the information needed to generate a
/// file. /// class for adding With* properties.
/// </summary> /// </summary>
internal class WithPropertyClass public class ClassAttributeReference
{ {
/// <summary> /// <summary>
/// Gets the syntax for the class declaration. /// Gets the syntax for the class declaration.

View file

@ -0,0 +1,66 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Base class for classes marked with an attribute.
/// </summary>
public abstract class ClassAttributeSourceGeneratorBase<TSyntaxReceiver>
: ISourceGenerator
where TSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
public void Execute(GeneratorExecutionContext context)
{
// Get the generator infrastructure will create a receiver and
// populate it we can retrieve the populated instance via the
// context.
if (context.SyntaxReceiver is not TSyntaxReceiver syntaxReceiver)
{
return;
}
// Report any messages.
foreach (string? message in syntaxReceiver.Messages)
{
context.Warning(
MessageCode.Debug,
Location.Create(
"Temporary.g.cs",
TextSpan.FromBounds(0, 0),
new LinePositionSpan(
new LinePosition(0, 0),
new LinePosition(0, 0))),
"{0}: Syntax Message: {1}",
this.GetType().Name,
message);
}
// If we didn't find anything, then there is nothing to do.
if (syntaxReceiver.ReferenceList.Count == 0)
{
return;
}
// Go through each one.
foreach (ClassAttributeReference reference in syntaxReceiver
.ReferenceList)
{
this.GenerateClassFile(context, reference);
}
}
public void Initialize(GeneratorInitializationContext context)
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(
() => this.CreateSyntaxReceiver(context));
}
protected abstract TSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context);
protected abstract void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference reference);
}

View file

@ -6,18 +6,26 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MfGames.Nitride.Generators; namespace MfGames.Nitride.Generators;
internal class WithPropertySyntaxReceiver : ISyntaxReceiver public abstract class ClassAttributeSyntaxReceiverBase : ISyntaxReceiver
{ {
private readonly string attributeName;
private readonly GeneratorInitializationContext context; private readonly GeneratorInitializationContext context;
public WithPropertySyntaxReceiver(GeneratorInitializationContext context) public ClassAttributeSyntaxReceiverBase(
GeneratorInitializationContext context,
string attributeName)
{ {
this.context = context; this.context = context;
this.ClassList = new List<WithPropertyClass>(); this.attributeName = attributeName;
this.ReferenceList = new List<ClassAttributeReference>();
this.Messages = new List<string>(); this.Messages = new List<string>();
} }
public List<WithPropertyClass> ClassList { get; } /// <summary>
/// Gets or sets a value indicating whether we should debug parsing attributes.
/// </summary>
public bool DebugAttributes { get; set; }
public List<string> Messages { get; } public List<string> Messages { get; }
@ -26,6 +34,8 @@ internal class WithPropertySyntaxReceiver : ISyntaxReceiver
/// </summary> /// </summary>
public string? Namespace { get; private set; } public string? Namespace { get; private set; }
public List<ClassAttributeReference> ReferenceList { get; }
public List<UsingDirectiveSyntax> UsingDirectiveList { get; set; } = new(); public List<UsingDirectiveSyntax> UsingDirectiveList { get; set; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode) public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
@ -64,21 +74,30 @@ internal class WithPropertySyntaxReceiver : ISyntaxReceiver
} }
// See if the class has our set properties attribute. // See if the class has our set properties attribute.
bool found = cds.AttributeLists.AsEnumerable() var attributes = cds.AttributeLists
.AsEnumerable()
.SelectMany(x => x.Attributes) .SelectMany(x => x.Attributes)
.Select(x => x.Name.ToString()) .Select(x => x.Name.ToString())
.ToList();
bool found = attributes
.Any( .Any(
x => x switch x => x == this.attributeName
{ || x == $"{this.attributeName}Attribute");
"WithProperties" => true,
"WithPropertiesAttribute" => true, if (this.DebugAttributes)
_ => false, {
}); this.Messages.Add(
string.Format(
"Parsing {0} found? {1} from attributes [{2}]",
cds.Identifier,
found,
string.Join(", ", attributes)));
}
if (found) if (found)
{ {
this.ClassList.Add( this.ReferenceList.Add(
new WithPropertyClass new ClassAttributeReference
{ {
Namespace = this.Namespace!, Namespace = this.Namespace!,
UsingDirectiveList = this.UsingDirectiveList, UsingDirectiveList = this.UsingDirectiveList,

View file

@ -18,7 +18,7 @@ public static class CodeAnalysisExtensions
this GeneratorExecutionContext context, this GeneratorExecutionContext context,
MessageCode messageCode, MessageCode messageCode,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
Error(context, messageCode, null, format, parameters); Error(context, messageCode, null, format, parameters);
} }
@ -36,7 +36,7 @@ public static class CodeAnalysisExtensions
MessageCode messageCode, MessageCode messageCode,
Location? location, Location? location,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
context.Message( context.Message(
messageCode, messageCode,
@ -57,7 +57,7 @@ public static class CodeAnalysisExtensions
this GeneratorExecutionContext context, this GeneratorExecutionContext context,
MessageCode messageCode, MessageCode messageCode,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
Information(context, messageCode, null, format, parameters); Information(context, messageCode, null, format, parameters);
} }
@ -75,7 +75,7 @@ public static class CodeAnalysisExtensions
MessageCode messageCode, MessageCode messageCode,
Location? location, Location? location,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
context.Message( context.Message(
messageCode, messageCode,
@ -96,7 +96,7 @@ public static class CodeAnalysisExtensions
this GeneratorExecutionContext context, this GeneratorExecutionContext context,
MessageCode messageCode, MessageCode messageCode,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
Warning(context, messageCode, null, format, parameters); Warning(context, messageCode, null, format, parameters);
} }
@ -114,7 +114,7 @@ public static class CodeAnalysisExtensions
MessageCode messageCode, MessageCode messageCode,
Location? location, Location? location,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
context.Message( context.Message(
messageCode, messageCode,
@ -139,7 +139,7 @@ public static class CodeAnalysisExtensions
Location? location, Location? location,
DiagnosticSeverity severity, DiagnosticSeverity severity,
string format, string format,
params object[] parameters) params object?[] parameters)
{ {
context.ReportDiagnostic( context.ReportDiagnostic(
Diagnostic.Create( Diagnostic.Create(

View file

@ -10,10 +10,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0"/> <PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1"/> <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1"/> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1"/> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -0,0 +1,90 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Implements a source generator that creates the additional properties
/// and methods for a singleton component including the constructor and
/// instance methods, along with extension methods for adding them to entities.
/// </summary>
[Generator]
public class SingletonComponentSourceGenerator
: ClassAttributeSourceGeneratorBase<SingletonComponentSyntaxReceiver>
{
protected override SingletonComponentSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context)
{
return new SingletonComponentSyntaxReceiver(context);
}
protected override void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference unit)
{
// Pull out some fields.
ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class.
StringBuilder buffer = new();
buffer.AppendLine("#nullable enable");
// Copy the using statements from the file.
foreach (UsingDirectiveSyntax? uds in unit.UsingDirectiveList)
{
buffer.AppendLine(uds.ToString());
}
buffer.AppendLine();
// Create the namespace.
SyntaxToken cls = cds.Identifier;
buffer.AppendLine(string.Join(
"\n",
$"using MfGames.Gallium;",
$"",
$"namespace {unit.Namespace}",
$"{{",
$" public partial class {cls}",
$" {{",
$" static {cls}()",
$" {{",
$" Instance = new {cls}();",
$" }}",
$"",
$" private {cls}()",
$" {{",
$" }}",
$"",
$" public static {cls} Instance {{ get; }}",
$" }}",
$"",
$" public static class {cls}Extensions",
$" {{",
$" public static bool Has{cls}(this Entity entity)",
$" {{",
$" return entity.Has<{cls}>();",
$" }}",
$"",
$" public static Entity Remove{cls}(this Entity entity)",
$" {{",
$" return entity.Remove<{cls}>();",
$" }}",
$"",
$" public static Entity Set{cls}(this Entity entity)",
$" {{",
$" return entity.Set({cls}.Instance);",
$" }}",
$" }}",
$"}}",
""));
// Create the source text and write out the file.
var sourceText = SourceText.From(buffer.ToString(), Encoding.UTF8);
context.AddSource(cls + ".Generated.cs", sourceText);
}
}

View file

@ -0,0 +1,13 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
public class SingletonComponentSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
/// <inheritdoc />
public SingletonComponentSyntaxReceiver(
GeneratorInitializationContext context)
: base(context, "SingletonComponent")
{
}
}

View file

@ -15,62 +15,22 @@ namespace MfGames.Nitride.Generators;
/// together calls. /// together calls.
/// </summary> /// </summary>
[Generator] [Generator]
public class WithPropertySourceGenerator : ISourceGenerator public class WithPropertiesSourceGenerator
: ClassAttributeSourceGeneratorBase<WithPropertiesSyntaxReceiver>
{ {
public void Execute(GeneratorExecutionContext context) /// <inheritdoc />
protected override WithPropertiesSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context)
{ {
// Get the generator infrastructure will create a receiver and return new WithPropertiesSyntaxReceiver(context);
// populate it we can retrieve the populated instance via the
// context.
var syntaxReceiver =
(WithPropertySyntaxReceiver?)context.SyntaxReceiver;
if (syntaxReceiver == null)
{
return;
}
// Report any messages.
foreach (string? message in syntaxReceiver.Messages)
{
context.Information(
MessageCode.Debug,
Location.Create(
"Temporary.g.cs",
TextSpan.FromBounds(0, 0),
new LinePositionSpan(
new LinePosition(0, 0),
new LinePosition(0, 0))),
"Generating additional identifier code: {0}",
message);
}
// If we didn't find anything, then there is nothing to do.
if (syntaxReceiver.ClassList.Count == 0)
{
return;
}
// Go through each one.
foreach (WithPropertyClass classInfo in syntaxReceiver.ClassList)
{
this.GenerateClassFile(context, classInfo);
}
} }
public void Initialize(GeneratorInitializationContext context) protected override void GenerateClassFile(
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(
() => new WithPropertySyntaxReceiver(context));
}
private void GenerateClassFile(
GeneratorExecutionContext context, GeneratorExecutionContext context,
WithPropertyClass unit) ClassAttributeReference unit)
{ {
// Pull out some fields. // Pull out some fields.
ClassDeclarationSyntax? cds = unit.ClassDeclaration; ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class. // Create the partial class.
StringBuilder buffer = new(); StringBuilder buffer = new();
@ -100,9 +60,8 @@ public class WithPropertySourceGenerator : ISourceGenerator
foreach (PropertyDeclarationSyntax pds in properties) foreach (PropertyDeclarationSyntax pds in properties)
{ {
// See if we have a setter. // See if we have a setter.
bool found = bool found = pds.AccessorList?.Accessors
pds.AccessorList?.Accessors.Any( .Any(x => x.Keyword.ToString() == "set")
x => x.Keyword.ToString() == "set")
?? false; ?? false;
if (!found) if (!found)

View file

@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
public class WithPropertiesSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
/// <inheritdoc />
public WithPropertiesSyntaxReceiver(GeneratorInitializationContext context)
: base(context, "WithProperties")
{
}
}

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Html; namespace MfGames.Nitride.Html;
/// <summary> /// <summary>
/// A marker component that indicates that the entity is an HTML file. /// A marker component that indicates that the entity is an HTML file.
/// </summary> /// </summary>
public record IsHtml [SingletonComponent]
public partial class IsHtml
{ {
public static IsHtml Instance { get; } = new();
} }

View file

@ -33,7 +33,8 @@ public partial class AddPathPrefix : OperationBase
{ {
this.validator.ValidateAndThrow(this); this.validator.ValidateAndThrow(this);
return this.replacePath.WithReplacement(this.RunReplacement) return this.replacePath
.WithReplacement(this.RunReplacement)
.Run(input); .Run(input);
} }

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Json; namespace MfGames.Nitride.Json;
/// <summary> /// <summary>
/// A marker class that indicates that the entity is JSON. /// A marker class that indicates that the entity is JSON.
/// </summary> /// </summary>
public record IsJson [SingletonComponent]
public partial class IsJson
{ {
public static IsJson Instance { get; } = new();
} }

View file

@ -1,16 +0,0 @@
using MfGames.Gallium;
namespace MfGames.Nitride.Json;
public static class IsJsonExtensions
{
public static Entity RemoveIsJson(this Entity entity)
{
return entity.Remove<IsJson>();
}
public static Entity SetIsJson(this Entity entity)
{
return entity.Set(IsJson.Instance);
}
}

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Markdown; namespace MfGames.Nitride.Markdown;
/// <summary> /// <summary>
/// A marker class that indicates that the file is a Markdown file. /// A marker class that indicates that the file is a Markdown file.
/// </summary> /// </summary>
public record IsMarkdown [SingletonComponent]
public partial class IsMarkdown
{ {
public static IsMarkdown Instance { get; } = new();
} }

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Yaml; namespace MfGames.Nitride.Yaml;
/// <summary> /// <summary>
/// A marker class that indicates that the entity is YAML. /// A marker class that indicates that the entity is YAML.
/// </summary> /// </summary>
public record IsYaml [SingletonComponent]
public partial class IsYaml
{ {
public static IsYaml Instance { get; } = new();
} }

View file

@ -1,16 +0,0 @@
using MfGames.Gallium;
namespace MfGames.Nitride.Yaml;
public static class IsYamlExtensions
{
public static Entity RemoveIsYaml(this Entity entity)
{
return entity.Remove<IsYaml>();
}
public static Entity SetIsYaml(this Entity entity)
{
return entity.Set(IsYaml.Instance);
}
}

View file

@ -10,12 +10,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/> <ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0"/> <PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="YamlDotNet" Version="12.0.0"/> <PackageReference Include="YamlDotNet" Version="12.0.0" />
</ItemGroup> </ItemGroup>
<!-- Include the source generator --> <!-- Include the source generator -->