210 lines
7 KiB
C#
210 lines
7 KiB
C#
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using System.Text;
|
||
|
using Microsoft.CodeAnalysis;
|
||
|
using Microsoft.CodeAnalysis.CSharp;
|
||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||
|
using Microsoft.CodeAnalysis.Text;
|
||
|
|
||
|
namespace Nitride.Generators
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Implements a source generator that creates Set* methods for the various
|
||
|
/// properties that also returns the same object for purposes of chaining
|
||
|
/// together calls.
|
||
|
/// </summary>
|
||
|
[Generator]
|
||
|
public class WithPropertySourceGenerator : ISourceGenerator
|
||
|
{
|
||
|
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.
|
||
|
var syntaxReceiver = (SyntaxReceiver?)context.SyntaxReceiver;
|
||
|
|
||
|
if (syntaxReceiver == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Report any messages.
|
||
|
foreach (var 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.ClassesToAugment.Count == 0)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Go through each one.
|
||
|
foreach (var cds in syntaxReceiver.ClassesToAugment)
|
||
|
{
|
||
|
this.GenerateClassFile(context, cds);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void Initialize(GeneratorInitializationContext context)
|
||
|
{
|
||
|
// Register a factory that can create our custom syntax receiver
|
||
|
context.RegisterForSyntaxNotifications(
|
||
|
() => new SyntaxReceiver(context));
|
||
|
}
|
||
|
|
||
|
private void GenerateClassFile(
|
||
|
GeneratorExecutionContext context,
|
||
|
ClassDeclarationSyntax cds)
|
||
|
{
|
||
|
// Get the namespace.
|
||
|
var nds = (NamespaceDeclarationSyntax?)cds.Parent;
|
||
|
|
||
|
if (nds == null)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
string? ns = nds.Name.ToString();
|
||
|
|
||
|
// Create the partial class.
|
||
|
StringBuilder buffer = new();
|
||
|
buffer.AppendLine("#nullable enable");
|
||
|
|
||
|
// Copy the using statements from the file.
|
||
|
if (nds.Parent is CompilationUnitSyntax cus)
|
||
|
{
|
||
|
foreach (var uds in cus.Usings)
|
||
|
{
|
||
|
buffer.AppendLine(uds.ToString());
|
||
|
}
|
||
|
|
||
|
buffer.AppendLine();
|
||
|
}
|
||
|
|
||
|
// Create the namespace.
|
||
|
buffer.AppendLine($"namespace {ns}");
|
||
|
buffer.AppendLine("{");
|
||
|
buffer.AppendLine($" public partial class {cds.Identifier}");
|
||
|
buffer.AppendLine(" {");
|
||
|
|
||
|
// Go through the properties of the namespace.
|
||
|
IEnumerable<PropertyDeclarationSyntax> properties = cds.Members
|
||
|
.Where(m => m.Kind() == SyntaxKind.PropertyDeclaration)
|
||
|
.Cast<PropertyDeclarationSyntax>();
|
||
|
bool first = true;
|
||
|
|
||
|
foreach (PropertyDeclarationSyntax pds in properties)
|
||
|
{
|
||
|
// See if we have a setter.
|
||
|
bool found =
|
||
|
pds.AccessorList?.Accessors
|
||
|
.Any(x => x.Keyword.ToString() == "set")
|
||
|
?? false;
|
||
|
|
||
|
if (!found)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// If we aren't first, then add a newline before it.
|
||
|
if (first)
|
||
|
{
|
||
|
first = false;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
buffer.AppendLine();
|
||
|
}
|
||
|
|
||
|
// Write some documentation.
|
||
|
buffer.AppendLine(" /// <summary>");
|
||
|
buffer.AppendLine(
|
||
|
string.Format(
|
||
|
" /// Sets the {0} value and returns the operation for chaining.",
|
||
|
pds.Identifier));
|
||
|
buffer.AppendLine(" /// </summary>");
|
||
|
|
||
|
// We have the components for writing out a setter.
|
||
|
buffer.AppendLine(
|
||
|
string.Format(
|
||
|
" public {0} With{1}({2} value)",
|
||
|
cds.Identifier,
|
||
|
pds.Identifier,
|
||
|
pds.Type));
|
||
|
buffer.AppendLine(" {");
|
||
|
buffer.AppendLine(
|
||
|
string.Format(
|
||
|
" this.{0} = value;",
|
||
|
pds.Identifier));
|
||
|
buffer.AppendLine(" return this;");
|
||
|
buffer.AppendLine(" }");
|
||
|
}
|
||
|
|
||
|
// Finish up the class.
|
||
|
buffer.AppendLine(" }");
|
||
|
buffer.AppendLine("}");
|
||
|
|
||
|
// Create the source text and write out the file.
|
||
|
SourceText sourceText = SourceText.From(
|
||
|
buffer.ToString(),
|
||
|
Encoding.UTF8);
|
||
|
context.AddSource(cds.Identifier + ".Generated.cs", sourceText);
|
||
|
}
|
||
|
|
||
|
private class SyntaxReceiver : ISyntaxReceiver
|
||
|
{
|
||
|
private readonly GeneratorInitializationContext context;
|
||
|
|
||
|
public SyntaxReceiver(GeneratorInitializationContext context)
|
||
|
{
|
||
|
this.context = context;
|
||
|
this.ClassesToAugment = new List<ClassDeclarationSyntax>();
|
||
|
this.Messages = new List<string>();
|
||
|
}
|
||
|
|
||
|
public List<ClassDeclarationSyntax> ClassesToAugment { get; }
|
||
|
|
||
|
public List<string> Messages { get; }
|
||
|
|
||
|
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
|
||
|
{
|
||
|
// We only care about class declarations.
|
||
|
if (syntaxNode is not ClassDeclarationSyntax cds)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// See if the class has our set properties attribute.
|
||
|
bool found = cds
|
||
|
.AttributeLists
|
||
|
.AsEnumerable()
|
||
|
.SelectMany(x => x.Attributes)
|
||
|
.Select(x => x.Name.ToString())
|
||
|
.Any(
|
||
|
x => x switch
|
||
|
{
|
||
|
"WithProperties" => true,
|
||
|
"WithPropertiesAttribute" => true,
|
||
|
_ => false,
|
||
|
});
|
||
|
|
||
|
if (found)
|
||
|
{
|
||
|
this.ClassesToAugment.Add(cds);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|