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

View File

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Calendar;
/// <summary>
/// A marker component for identifying an entity that represents a calendar.
/// </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;
/// <summary>
/// A marker component that indicates this page is a feed.
/// </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;
/// <summary>
/// A marker component for indicating that an entity is Gemtext, the format
/// for text files using the Gemini protocol.
/// </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>
/// Internal class that consolidates all of the information needed to generate a
/// file.
/// class for adding With* properties.
/// </summary>
internal class WithPropertyClass
public class ClassAttributeReference
{
/// <summary>
/// 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;
internal class WithPropertySyntaxReceiver : ISyntaxReceiver
public abstract class ClassAttributeSyntaxReceiverBase : ISyntaxReceiver
{
private readonly string attributeName;
private readonly GeneratorInitializationContext context;
public WithPropertySyntaxReceiver(GeneratorInitializationContext context)
public ClassAttributeSyntaxReceiverBase(
GeneratorInitializationContext context,
string attributeName)
{
this.context = context;
this.ClassList = new List<WithPropertyClass>();
this.attributeName = attributeName;
this.ReferenceList = new List<ClassAttributeReference>();
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; }
@ -26,6 +34,8 @@ internal class WithPropertySyntaxReceiver : ISyntaxReceiver
/// </summary>
public string? Namespace { get; private set; }
public List<ClassAttributeReference> ReferenceList { get; }
public List<UsingDirectiveSyntax> UsingDirectiveList { get; set; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
@ -64,21 +74,30 @@ internal class WithPropertySyntaxReceiver : ISyntaxReceiver
}
// See if the class has our set properties attribute.
bool found = cds.AttributeLists.AsEnumerable()
var attributes = cds.AttributeLists
.AsEnumerable()
.SelectMany(x => x.Attributes)
.Select(x => x.Name.ToString())
.ToList();
bool found = attributes
.Any(
x => x switch
{
"WithProperties" => true,
"WithPropertiesAttribute" => true,
_ => false,
});
x => x == this.attributeName
|| x == $"{this.attributeName}Attribute");
if (this.DebugAttributes)
{
this.Messages.Add(
string.Format(
"Parsing {0} found? {1} from attributes [{2}]",
cds.Identifier,
found,
string.Join(", ", attributes)));
}
if (found)
{
this.ClassList.Add(
new WithPropertyClass
this.ReferenceList.Add(
new ClassAttributeReference
{
Namespace = this.Namespace!,
UsingDirectiveList = this.UsingDirectiveList,

View File

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

View File

@ -10,10 +10,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Common" 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="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
</ItemGroup>
</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.
/// </summary>
[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
// 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);
}
return new WithPropertiesSyntaxReceiver(context);
}
public void Initialize(GeneratorInitializationContext context)
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(
() => new WithPropertySyntaxReceiver(context));
}
private void GenerateClassFile(
protected override void GenerateClassFile(
GeneratorExecutionContext context,
WithPropertyClass unit)
ClassAttributeReference unit)
{
// Pull out some fields.
ClassDeclarationSyntax? cds = unit.ClassDeclaration;
ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class.
StringBuilder buffer = new();
@ -100,9 +60,8 @@ public class WithPropertySourceGenerator : ISourceGenerator
foreach (PropertyDeclarationSyntax pds in properties)
{
// See if we have a setter.
bool found =
pds.AccessorList?.Accessors.Any(
x => x.Keyword.ToString() == "set")
bool found = pds.AccessorList?.Accessors
.Any(x => x.Keyword.ToString() == "set")
?? false;
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;
/// <summary>
/// A marker component that indicates that the entity is an HTML file.
/// </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);
return this.replacePath.WithReplacement(this.RunReplacement)
return this.replacePath
.WithReplacement(this.RunReplacement)
.Run(input);
}

View File

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Json;
/// <summary>
/// A marker class that indicates that the entity is JSON.
/// </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;
/// <summary>
/// A marker class that indicates that the file is a Markdown file.
/// </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;
/// <summary>
/// A marker class that indicates that the entity is YAML.
/// </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>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0"/>
<PackageReference Include="YamlDotNet" Version="12.0.0"/>
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="YamlDotNet" Version="12.0.0" />
</ItemGroup>
<!-- Include the source generator -->