Skip to content

Commit

Permalink
Refactored template storage
Browse files Browse the repository at this point in the history
Refactores DTO mechanism
Moved license file
  • Loading branch information
MelGrubb committed Jan 14, 2024
1 parent 0add83e commit 723d7d2
Show file tree
Hide file tree
Showing 27 changed files with 395 additions and 262 deletions.
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021-2023 Mel Grubb
Copyright (c) 2021-2024 Mel Grubb

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Install-Package BuilderGenerator
After installation, create a partial class to define your builder in. Decorate it with the ```BuilderFor``` attribute, specifying the type of class that the builder is meant to build (e.g. ```[BuilderFor(typeof(Foo))]```. Define any factory and helper methods in this partial class. Meanwhile, another partial class definition will be auto-generated which contains all the "boring" parts such as the backing fields and "with" methods.

## Version History ##
- v2.4.0
- Test code reorganization
- Moved WithObject from the base class to the generated builder class
- Added WithValuesFrom method to shallow clone an example object.

- v2.3.0
- Major caching and performance improvements
- Internal code cleanup
Expand Down
21 changes: 11 additions & 10 deletions src/BuilderGenerator.Tests.Unit/Examples/OutputWithInternals.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#nullable disable

//------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by BuilderGenerator at 2024-01-13T22:02:03 in 863.1781ms.
// This code was generated by BuilderGenerator at 2024-01-14T14:02:29 in 1232.3874ms
// </auto-generated>
//------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------
using System.CodeDom.Compiler;
using BuilderGenerator;

Expand All @@ -27,13 +27,13 @@ public PersonBuilderWithInternals(BuilderGenerator.UnitTests.Person value = null
}
}

/// <summary>Populates this instance with values from the provided template.</summary>
/// <remarks>This is a shallow clone, and does not traverse the template creating builders for its properties.</remarks>
public PersonBuilderWithInternals FromTemplate(BuilderGenerator.UnitTests.Person template)
/// <summary>Populates this instance with values from the provided example.</summary>
/// <remarks>This is a shallow clone, and does not traverse the example object creating builders for its properties.</remarks>
public PersonBuilderWithInternals WithValuesFrom(BuilderGenerator.UnitTests.Person example)
{
WithFirstName(template.FirstName);
WithInternalString(template.InternalString);
WithLastName(template.LastName);
WithFirstName(example.FirstName);
WithInternalString(example.InternalString);
WithLastName(example.LastName);

return this;
}
Expand All @@ -59,13 +59,14 @@ public override BuilderGenerator.UnitTests.Person Build()

return Object.Value;
}

/// <summary>Sets the object to be returned by this instance.</summary>
/// <param name="value">The object to be returned.</param>
/// <returns>A reference to this builder instance.</returns>
public PersonBuilderWithInternals WithObject(BuilderGenerator.UnitTests.Person value)
{
Object = new System.Lazy<BuilderGenerator.UnitTests.Person>(() => value);
FromTemplate(value);
WithValuesFrom(value);

return this;
}
Expand Down
19 changes: 10 additions & 9 deletions src/BuilderGenerator.Tests.Unit/Examples/OutputWithoutInternals.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#nullable disable

//------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by BuilderGenerator at 2024-01-13T22:03:19 in 3.8837ms.
// This code was generated by BuilderGenerator at 2024-01-14T14:03:02 in 4.9846ms
// </auto-generated>
//------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------
using System.CodeDom.Compiler;
using BuilderGenerator;

Expand All @@ -26,12 +26,12 @@ public PersonBuilderWithoutInternals(BuilderGenerator.UnitTests.Person value = n
}
}

/// <summary>Populates this instance with values from the provided template.</summary>
/// <remarks>This is a shallow clone, and does not traverse the template creating builders for its properties.</remarks>
public PersonBuilderWithoutInternals FromTemplate(BuilderGenerator.UnitTests.Person template)
/// <summary>Populates this instance with values from the provided example.</summary>
/// <remarks>This is a shallow clone, and does not traverse the example object creating builders for its properties.</remarks>
public PersonBuilderWithoutInternals WithValuesFrom(BuilderGenerator.UnitTests.Person example)
{
WithFirstName(template.FirstName);
WithLastName(template.LastName);
WithFirstName(example.FirstName);
WithLastName(example.LastName);

return this;
}
Expand All @@ -56,13 +56,14 @@ public override BuilderGenerator.UnitTests.Person Build()

return Object.Value;
}

/// <summary>Sets the object to be returned by this instance.</summary>
/// <param name="value">The object to be returned.</param>
/// <returns>A reference to this builder instance.</returns>
public PersonBuilderWithoutInternals WithObject(BuilderGenerator.UnitTests.Person value)
{
Object = new System.Lazy<BuilderGenerator.UnitTests.Person>(() => value);
FromTemplate(value);
WithValuesFrom(value);

return this;
}
Expand Down
18 changes: 18 additions & 0 deletions src/BuilderGenerator.Tests.Unit/Templates/MyTemplates.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using BuilderGenerator.Templates;

namespace BuilderGenerator.Tests.Unit.Templates;

/// <summary>Defines custom code generation templates for testing.</summary>
/// <remarks>This is here to test the custom template mechanism itself by overriding individual pieces.</remarks>
internal class MyTemplates : CSharp11
{
// TODO: Override the BuilderClass template, changing the header to remove the timestamp only so that the results match.
public override string BuilderClass
{
get
{
return base.BuilderClass[base.BuilderClass.IndexOf("using", StringComparison.OrdinalIgnoreCase)..];
}
}
}
1 change: 0 additions & 1 deletion src/BuilderGenerator.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\LICENSE.txt = ..\LICENSE.txt
Publish-Local.ps1 = Publish-Local.ps1
..\README.md = ..\README.md
Refresh.ps1 = Refresh.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{16BA5C10-6F7A-4CDA-A88E-A4247FFEFC80}"
Expand Down
102 changes: 34 additions & 68 deletions src/BuilderGenerator/BuilderGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using BuilderGenerator.Diagnostics;
using BuilderGenerator.Templates;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
Expand All @@ -16,55 +15,20 @@ namespace BuilderGenerator;
[Generator]
internal class BuilderGenerator : IIncrementalGenerator
{
private static readonly string BuilderBaseClass;
private static readonly string BuilderClass;
private static readonly string BuilderForAttribute;
private static readonly string BuildMethod;
private static readonly string BuildMethodSetter;
private static readonly string Constructors;
private static readonly string FromTemplateMethod;
private static readonly string FromTemplateMethodSetter;
#pragma warning disable RS1035
private static readonly string NewLine = Environment.NewLine; // TODO: Derive this value from the project itself
#pragma warning restore RS1035
private static readonly string Property;
private static readonly string WithMethods;
private static readonly string WithObjectMethod;

static BuilderGenerator()
{
var assembly = typeof(BuilderGenerator).Assembly;

BuilderBaseClass = GetResourceAsString(assembly, nameof(BuilderBaseClass));
BuilderClass = GetResourceAsString(assembly, nameof(BuilderClass));
BuilderForAttribute = GetResourceAsString(assembly, nameof(BuilderForAttribute));
BuildMethodSetter = GetResourceAsString(assembly, nameof(BuildMethodSetter));
BuildMethod = GetResourceAsString(assembly, nameof(BuildMethod));
Constructors = GetResourceAsString(assembly, nameof(Constructors));
FromTemplateMethod = GetResourceAsString(assembly, nameof(FromTemplateMethod));
FromTemplateMethodSetter = GetResourceAsString(assembly, nameof(FromTemplateMethodSetter));
Property = GetResourceAsString(assembly, nameof(Property));
WithMethods = GetResourceAsString(assembly, nameof(WithMethods));
WithObjectMethod = GetResourceAsString(assembly, nameof(WithObjectMethod));
}

public static string GetResourceAsString(Assembly assembly, string resourceName)
{
resourceName = assembly.GetManifestResourceNames().Single(x => x.Equals($"BuilderGenerator.Templates.{resourceName}.txt", StringComparison.OrdinalIgnoreCase));
using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new InvalidOperationException($"Resource '{resourceName}' not found.");
using var reader = new StreamReader(stream);

return reader.ReadToEnd();
}
private static readonly CSharp Templates = new CSharp11();

public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Register injection of classes that never change.
// Register injection of classes to be injected as-is.
context.RegisterPostInitializationOutput(
x =>
{
x.AddSource(nameof(BuilderBaseClass), SourceText.From(BuilderBaseClass, Encoding.UTF8));
x.AddSource(nameof(BuilderForAttribute), SourceText.From(BuilderForAttribute, Encoding.UTF8));
x.AddSource(nameof(Templates.BuilderBaseClass), SourceText.From(Templates.BuilderBaseClass, Encoding.UTF8));
x.AddSource(nameof(Templates.BuilderForAttribute), SourceText.From(Templates.BuilderForAttribute, Encoding.UTF8));
});

// Register generation for classes based on the project contents
Expand All @@ -91,11 +55,11 @@ private static void Execute(SourceProductionContext context, BuilderInfo? builde
templateParser.SetTag("TargetClassFullName", builder.Value.TargetClassFullName);
templateParser.SetTag("Constructors", GenerateConstructors(templateParser));
templateParser.SetTag("Properties", GenerateProperties(templateParser, builder.Value.Properties));
templateParser.SetTag("FromTemplateMethod", GenerateFromTemplateMethod(templateParser, builder.Value.Properties));
templateParser.SetTag("WithValuesFromMethod", GenerateWithValuesFromMethod(templateParser, builder.Value.Properties));
templateParser.SetTag("BuildMethod", GenerateBuildMethod(templateParser, builder.Value.Properties));
templateParser.SetTag("WithMethods", GenerateWithMethods(templateParser, builder.Value.Properties));
templateParser.SetTag("WithObjectMethod", GenerateWithObjectMethod(templateParser));
var source = templateParser.ParseString(BuilderClass);
var source = templateParser.ParseString(Templates.BuilderClass);
context.AddSource($"{builder.Value.BuilderClassName}.g.cs", SourceText.From(source, Encoding.UTF8));
}
catch (Exception e)
Expand All @@ -104,7 +68,7 @@ private static void Execute(SourceProductionContext context, BuilderInfo? builde
}
}

private static string GenerateBuildMethod(TemplateParser templateParser, IEnumerable<PropertyInfo> properties)
private static string GenerateBuildMethod(TemplateParser templateParser, IEnumerable<BuilderInfo.PropertyInfo> properties)
{
var setters = string.Join(
NewLine,
Expand All @@ -113,39 +77,37 @@ private static string GenerateBuildMethod(TemplateParser templateParser, IEnumer
{
templateParser.SetTag("PropertyName", p.Name);

return templateParser.ParseString(BuildMethodSetter);
return templateParser.ParseString(Templates.BuildMethodSetter);
}));

templateParser.SetTag("Setters", setters);
var result = templateParser.ParseString(BuildMethod);
var result = templateParser.ParseString(Templates.BuildMethod);

return result;
}

private static string GenerateConstructors(TemplateParser templateParser)
{
return templateParser.ParseString(Constructors);
return templateParser.ParseString(Templates.Constructors);
}

private static string GenerateFromTemplateMethod(TemplateParser templateParser, IEnumerable<PropertyInfo> properties)
private static string GenerateProperties(TemplateParser templateParser, IEnumerable<BuilderInfo.PropertyInfo> properties)
{
var setters = string.Join(
var result = string.Join(
NewLine,
properties.Select(
p =>
{
templateParser.SetTag("PropertyName", p.Name);
templateParser.SetTag("PropertyType", p.Type);

return templateParser.ParseString(FromTemplateMethodSetter);
return templateParser.ParseString(Templates.Property);
}));

templateParser.SetTag("FromTemplateMethodSetters", setters);
var result = templateParser.ParseString(FromTemplateMethod);

return result;
}

private static string GenerateProperties(TemplateParser templateParser, IEnumerable<PropertyInfo> properties)
private static string GenerateWithMethods(TemplateParser templateParser, IEnumerable<BuilderInfo.PropertyInfo> properties)
{
var result = string.Join(
NewLine,
Expand All @@ -155,31 +117,33 @@ private static string GenerateProperties(TemplateParser templateParser, IEnumera
templateParser.SetTag("PropertyName", p.Name);
templateParser.SetTag("PropertyType", p.Type);

return templateParser.ParseString(Property);
return templateParser.ParseString(Templates.WithMethods);
}));

return result;
}

private static string GenerateWithMethods(TemplateParser templateParser, IEnumerable<PropertyInfo> properties)
private static string GenerateWithObjectMethod(TemplateParser templateParser)
{
var result = string.Join(
return templateParser.ParseString(Templates.WithObjectMethod);
}

private static string GenerateWithValuesFromMethod(TemplateParser templateParser, IEnumerable<BuilderInfo.PropertyInfo> properties)
{
var setters = string.Join(
NewLine,
properties.Select(
p =>
{
templateParser.SetTag("PropertyName", p.Name);
templateParser.SetTag("PropertyType", p.Type);

return templateParser.ParseString(WithMethods);
return templateParser.ParseString(Templates.WithValuesFromSetter);
}));

return result;
}
templateParser.SetTag("WithValuesFromSetters", setters);
var result = templateParser.ParseString(Templates.WithValuesFromMethod);

private static string GenerateWithObjectMethod(TemplateParser templateParser)
{
return templateParser.ParseString(WithObjectMethod);
return result;
}

private static IEnumerable<IPropertySymbol> GetPropertySymbols(INamedTypeSymbol namedTypeSymbol, bool includeInternals)
Expand All @@ -200,10 +164,10 @@ private static IEnumerable<IPropertySymbol> GetPropertySymbols(INamedTypeSymbol
return symbols;
}

/// <summary>Performs a first-pass filtering of syntax nodes that might possibly represent a builder class.</summary>
/// <summary>Performs a first-pass filtering of syntax nodes that could possibly represent a builder class.</summary>
/// <param name="node">The syntax node being examined.</param>
/// <param name="_">A cancellation token (currently unused).</param>
/// <returns>A <see cref="bool" /> indicating whether or not <paramref name="node" /> might possibly represent a builder class.</returns>
/// <returns>A <see cref="bool" /> indicating whether <paramref name="node" /> might possibly represent a builder class.</returns>
private static bool Predicate(SyntaxNode node, CancellationToken _)
{
return node is TypeDeclarationSyntax { AttributeLists.Count: > 0 };
Expand All @@ -226,7 +190,7 @@ private static bool Predicate(SyntaxNode node, CancellationToken _)

if (!namedTypeSymbol.GetAttributes().Any(x => x.AttributeClass?.Name == "BuilderForAttribute")) { return null; }

var attributeSymbol = namedTypeSymbol.GetAttributes().SingleOrDefault(x => x.AttributeClass!.Name == nameof(BuilderForAttribute));
var attributeSymbol = namedTypeSymbol.GetAttributes().SingleOrDefault(x => x.AttributeClass!.Name == nameof(Templates.BuilderForAttribute));

if (attributeSymbol is null) { return null; }

Expand All @@ -250,14 +214,16 @@ private static bool Predicate(SyntaxNode node, CancellationToken _)
TargetClassFullName = targetClassType.Value!.ToString(),
BuilderClassUsingBlock = ((CompilationUnitSyntax)typeNode.SyntaxTree.GetRoot()).Usings.ToString(),
Properties = targetClassProperties.Select(
x => new PropertyInfo
x => new BuilderInfo.PropertyInfo
{
Accessibility = x.Accessibility,
Name = x.Name,
Type = x.TypeName,
}).ToList(),
Location = typeNode.GetLocation(),
Identifier = typeNode.Identifier.ToString(),

// TODO: Retrieve template class instance/type from attribute, and use that to retrieve the strings later on.
TimeToGenerate = stopwatch.Elapsed,
};

Expand Down
Loading

0 comments on commit 723d7d2

Please sign in to comment.