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 { /// /// Implements a source generator that creates Set* methods for the various /// properties that also returns the same object for purposes of chaining /// together calls. /// [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 properties = cds.Members .Where(m => m.Kind() == SyntaxKind.PropertyDeclaration) .Cast(); 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(" /// "); buffer.AppendLine( string.Format( " /// Sets the {0} value and returns the operation for chaining.", pds.Identifier)); buffer.AppendLine(" /// "); // 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(); this.Messages = new List(); } public List ClassesToAugment { get; } public List 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); } } } } }