diff --git a/README.md b/README.md index a09d80b..2cbfb28 100644 --- a/README.md +++ b/README.md @@ -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.3.0 + - Major caching and performance improvements + - Internal code cleanup + - Conversion of templates to embedded resources + - v2.2.0 - Changed generated file extension to .g.cs diff --git a/src/BuilderGenerator.IntegrationTests.Core/BuilderGenerator.IntegrationTests.Core.csproj b/src/BuilderGenerator.IntegrationTests.Core/BuilderGenerator.IntegrationTests.Core.csproj index 156ce0a..d87e22c 100644 --- a/src/BuilderGenerator.IntegrationTests.Core/BuilderGenerator.IntegrationTests.Core.csproj +++ b/src/BuilderGenerator.IntegrationTests.Core/BuilderGenerator.IntegrationTests.Core.csproj @@ -7,10 +7,10 @@ - <_Parameter1>BuilderGenerator.IntegrationTests.Net60.FromPackage + <_Parameter1>BuilderGenerator.IntegrationTests.Net60 - <_Parameter1>BuilderGenerator.IntegrationTests.Net60.FromProject + <_Parameter1>BuilderGenerator.IntegrationTests.Net60 - + \ No newline at end of file diff --git a/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/User.cs b/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/User.cs index 818ce2a..0858bfd 100644 --- a/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/User.cs +++ b/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/User.cs @@ -4,10 +4,14 @@ namespace BuilderGenerator.IntegrationTests.Core.Models.Entities; -public class User : AuditableEntity +public partial class User : AuditableEntity { - public string? FirstName { get; set; } - public string? LastName { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } public string? MiddleName { get; set; } +} + +public partial class User +{ public ICollection Orders { get; set; } = new List(); } diff --git a/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/_ReadMe.md b/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/_ReadMe.md new file mode 100644 index 0000000..62bdb2a --- /dev/null +++ b/src/BuilderGenerator.IntegrationTests.Core/Models/Entities/_ReadMe.md @@ -0,0 +1,11 @@ +# Entities # + +This folder contains sample entities for which builders will be created. It's your standard, generic Foo/Bar/Baz hierarchy, with edge cases exercised as described blow. + +## Hierarchy ## + +Foo contains a read-only collection or Bars. +Bars contains a read-only collection of Bazs. +Baz has no children of its own. + +Each layer also contains writable collections of strings in various formats to test the Builder's ability to adapt and choose appropriate properties to handle. diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/BuilderGenerator.IntegrationTests.Net60.FromPackage.csproj b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/BuilderGenerator.IntegrationTests.Net60.FromPackage.csproj deleted file mode 100644 index c876e66..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/BuilderGenerator.IntegrationTests.Net60.FromPackage.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net6.0 - false - latest - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/OrderBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/OrderBuilder.cs deleted file mode 100644 index 801b16a..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/OrderBuilder.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using BuilderGenerator.IntegrationTests.Core.Models.Entities; - -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; - -[BuilderFor(typeof(Order), true)] -public partial class OrderBuilder -{ - public static OrderBuilder Simple() - { - var builder = new OrderBuilder() - .WithId(Guid.NewGuid); - - return builder; - } - - public static OrderBuilder Typical() - { - var builder = Simple() - .WithItems( - () => new List - { - OrderItemBuilder.Simple().Build(), - }); - - return builder; - } -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/OrderItemBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/OrderItemBuilder.cs deleted file mode 100644 index aa7d04b..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/OrderItemBuilder.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using BuilderGenerator.IntegrationTests.Core.Models.Entities; - -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; - -[BuilderFor(typeof(OrderItem))] -public partial class OrderItemBuilder -{ - public static OrderItemBuilder Simple() - { - var builder = new OrderItemBuilder() - .WithId(Guid.NewGuid); - - return builder; - } -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/UserBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/UserBuilder.cs deleted file mode 100644 index 9ee4189..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/UserBuilder.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using BuilderGenerator.IntegrationTests.Core.Models.Entities; - -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; - -[BuilderFor(typeof(User))] -public partial class UserBuilder -{ - public static UserBuilder Simple() - { - var builder = new UserBuilder() - .WithId(Guid.NewGuid) - .WithFirstName(() => Guid.NewGuid().ToString()) - .WithMiddleName(() => Guid.NewGuid().ToString()) - .WithLastName(() => Guid.NewGuid().ToString()); - - return builder; - } - - public static UserBuilder Typical() - { - var builder = Simple() - .WithOrders( - () => new List - { - OrderBuilder.Simple().Build(), - }); - - return builder; - } -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/README.md b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/README.md deleted file mode 100644 index 2a272ec..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# BuilderGenerator.Sample.Net60.FromPackage - -This project provides tests the BuilderGenerator package being used in a .Net 6.0 project using a simple series of inter-related classes defined in the BuiderGenerator.Test.Core project. - -The reference to the BuilderGenerator is a NuGet package reference, so it can be used to check the results of the final product. It is meant to test the deployment mechanism. diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/OrderBuilderTests.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/OrderBuilderTests.cs deleted file mode 100644 index 29a5bda..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/OrderBuilderTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using BuilderGenerator.IntegrationTests.Core.Models.Entities; -using BuilderGenerator.IntegrationTests.Core.Models.Enums; -using BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; -using NUnit.Framework; -using Shouldly; - -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Tests; - -[TestFixture] -public class OrderBuilderTests -{ - private Guid _id; - private string _internalString; - private DateTime _orderDate; - private Order _result; - private OrderStatus _status; - - [Test] - public void OrderBuilder_can_set_properties() - { - _result.Id.ShouldBe(_id); - _result.InternalString.ShouldBe(_internalString); - _result.OrderDate.ShouldBe(_orderDate); - _result.Status.ShouldBe(_status); - } - - [Test] - public void Simple_does_not_populate_Items() - { - var actual = OrderBuilder.Simple().Build(); - actual.ShouldBeOfType(); - actual.Items.ShouldBeNull(); - } - - [Test] - public void Simple_returns_an_OrderBuilder() => OrderBuilder.Simple().ShouldBeOfType(); - - [Test] - public void Typical_populates_Items() - { - var actual = OrderBuilder.Typical().Build(); - actual.ShouldBeOfType(); - actual.Items.ShouldNotBeNull(); - } - - [Test] - public void Typical_returns_an_OrderBuilder() => OrderBuilder.Typical().ShouldBeOfType(); - - [OneTimeSetUp] - public void SetUp() - { - _id = Guid.NewGuid(); - _orderDate = DateTime.Now; - _status = OrderStatus.Processing; - _internalString = Guid.NewGuid().ToString(); - - _result = OrderBuilder - .Typical() - .WithId(_id) - .WithInternalString(_internalString) - .WithOrderDate(_orderDate) - .WithStatus(_status) - .Build(); - } -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/UserBuilderTests.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/UserBuilderTests.cs deleted file mode 100644 index 5c6d875..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/UserBuilderTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using BuilderGenerator.IntegrationTests.Core.Models.Entities; -using BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; -using NUnit.Framework; -using Shouldly; - -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Tests; - -[TestFixture] -public class UserBuilderTests -{ - private string _firstName; - private Guid _id; - private string _lastName; - private string _middleName; - private User _result; - - [Test] - public void Simple_does_not_populate_Orders() - { - var actual = UserBuilder.Simple().Build(); - actual.ShouldBeOfType(); - actual.Orders.ShouldBeNull(); - } - - [Test] - public void Simple_returns_a_UserBuilder() => UserBuilder.Simple().ShouldBeOfType(); - - [Test] - public void Typical_populates_Orders() - { - var actual = UserBuilder.Typical().Build(); - actual.ShouldBeOfType(); - actual.Orders.ShouldNotBeNull(); - } - - [Test] - public void Typical_returns_a_UserBuilder() => UserBuilder.Typical().ShouldBeOfType(); - - [Test] - public void UserBuilder_can_set_properties() - { - _result.Id.ShouldBe(_id); - _result.FirstName.ShouldBe(_firstName); - _result.MiddleName.ShouldBe(_middleName); - _result.LastName.ShouldBe(_lastName); - } - - [OneTimeSetUp] - public void SetUp() - { - _id = Guid.NewGuid(); - _firstName = Guid.NewGuid().ToString(); - _middleName = Guid.NewGuid().ToString(); - _lastName = Guid.NewGuid().ToString(); - - _result = UserBuilder - .Typical() - .WithId(_id) - .WithFirstName(_firstName) - .WithMiddleName(_middleName) - .WithLastName(_lastName) - .Build(); - } -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/CollectionTypeSampleBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/CollectionTypeSampleBuilder.cs deleted file mode 100644 index 1a85d9a..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/CollectionTypeSampleBuilder.cs +++ /dev/null @@ -1,8 +0,0 @@ -using BuilderGenerator.IntegrationTests.Core.Models.Entities; - -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; - -[BuilderFor(typeof(CollectionTypesSample))] -public partial class CollectionTypeSampleBuilder -{ -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/README.md b/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/README.md deleted file mode 100644 index b84d4d2..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Partial Builders # - -This folder contains the hand-written half of any builders. Create static factory methods for each well-known or named object instance you want to create. - -## Suggestions ## - -In addition to specific factory methods, you should define some general-purpose methods such as: - -### Simple/Minimal ### - -Creates the simplest, most minimal instance that will pass validation. Only fill in the required fields, and leave everything else at their default values. This can serve as the starting point for other, more specific factory methods. For instance, a minimal Customer may be missing address information, and would have no orders. - -### Typical ### - -This method should return a "typical" example of the class. For a Customer entity, this might mean that the shipping and billing addresses are filled in, and the Customer has multiple orders in various states. \ No newline at end of file diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/BuilderTests.cs b/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/BuilderTests.cs deleted file mode 100644 index a81d839..0000000 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/BuilderTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using BuilderGenerator.IntegrationTests.Core.Models.Entities; -using BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; -using NUnit.Framework; -using Shouldly; - -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Tests; - -[TestFixture] -public class BuilderTests -{ - private string _firstName; - private Guid _id; - private string _lastName; - private string _middleName; - private User _result; - - [Test] - public void Simple_does_not_populate_Orders() - { - var actual = UserBuilder.Simple().Build(); - actual.ShouldBeOfType(); - actual.Orders.ShouldBeNull(); - } - - [Test] - public void Simple_returns_a_UserBuilder() => UserBuilder.Simple().ShouldBeOfType(); - - [Test] - public void Typical_populates_Orders() - { - var actual = UserBuilder.Typical().Build(); - actual.ShouldBeOfType(); - actual.Orders.ShouldNotBeNull(); - } - - [Test] - public void Typical_returns_a_UserBuilder() => UserBuilder.Typical().ShouldBeOfType(); - - [Test] - public void UserBuilder_can_set_properties() - { - _result.Id.ShouldBe(_id); - _result.FirstName.ShouldBe(_firstName); - _result.MiddleName.ShouldBe(_middleName); - _result.LastName.ShouldBe(_lastName); - } - - [OneTimeSetUp] - public void SetUp() - { - _id = Guid.NewGuid(); - _firstName = Guid.NewGuid().ToString(); - _middleName = Guid.NewGuid().ToString(); - _lastName = Guid.NewGuid().ToString(); - - _result = UserBuilder - .Typical() - .WithId(_id) - .WithFirstName(_firstName) - .WithMiddleName(_middleName) - .WithLastName(_lastName) - .Build(); - } -} diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/BuilderGenerator.IntegrationTests.Net60.FromProject.csproj b/src/BuilderGenerator.IntegrationTests.Net60/BuilderGenerator.IntegrationTests.Net60.csproj similarity index 100% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/BuilderGenerator.IntegrationTests.Net60.FromProject.csproj rename to src/BuilderGenerator.IntegrationTests.Net60/BuilderGenerator.IntegrationTests.Net60.csproj diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/CollectionTypeSampleBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60/Builders/CollectionTypeSampleBuilder.cs similarity index 69% rename from src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/CollectionTypeSampleBuilder.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Builders/CollectionTypeSampleBuilder.cs index 2e04081..0b0b23b 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/CollectionTypeSampleBuilder.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Builders/CollectionTypeSampleBuilder.cs @@ -1,6 +1,6 @@ using BuilderGenerator.IntegrationTests.Core.Models.Entities; -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; +namespace BuilderGenerator.IntegrationTests.Net60.Builders; [BuilderFor(typeof(CollectionTypesSample))] public partial class CollectionTypeSampleBuilder diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/OrderBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60/Builders/OrderBuilder.cs similarity index 89% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/OrderBuilder.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Builders/OrderBuilder.cs index e892db1..541c574 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/OrderBuilder.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Builders/OrderBuilder.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using BuilderGenerator.IntegrationTests.Core.Models.Entities; -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; +namespace BuilderGenerator.IntegrationTests.Net60.Builders; [BuilderFor(typeof(Order), true)] public partial class OrderBuilder diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/OrderItemBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60/Builders/OrderItemBuilder.cs similarity index 81% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/OrderItemBuilder.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Builders/OrderItemBuilder.cs index ee6dee6..8417de8 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/OrderItemBuilder.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Builders/OrderItemBuilder.cs @@ -1,7 +1,7 @@ using System; using BuilderGenerator.IntegrationTests.Core.Models.Entities; -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; +namespace BuilderGenerator.IntegrationTests.Net60.Builders; [BuilderFor(typeof(OrderItem))] public partial class OrderItemBuilder diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/README.md b/src/BuilderGenerator.IntegrationTests.Net60/Builders/README.md similarity index 100% rename from src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Builders/README.md rename to src/BuilderGenerator.IntegrationTests.Net60/Builders/README.md diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/UserBuilder.cs b/src/BuilderGenerator.IntegrationTests.Net60/Builders/UserBuilder.cs similarity index 91% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/UserBuilder.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Builders/UserBuilder.cs index ad16053..a2caba2 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Builders/UserBuilder.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Builders/UserBuilder.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using BuilderGenerator.IntegrationTests.Core.Models.Entities; -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; +namespace BuilderGenerator.IntegrationTests.Net60.Builders; [BuilderFor(typeof(User))] public partial class UserBuilder diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/README.md b/src/BuilderGenerator.IntegrationTests.Net60/README.md similarity index 100% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/README.md rename to src/BuilderGenerator.IntegrationTests.Net60/README.md diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/BuilderTests.cs b/src/BuilderGenerator.IntegrationTests.Net60/Tests/BuilderTests.cs similarity index 92% rename from src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/BuilderTests.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Tests/BuilderTests.cs index 2000a6a..a0a9369 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromPackage/Tests/BuilderTests.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Tests/BuilderTests.cs @@ -1,10 +1,10 @@ using System; using BuilderGenerator.IntegrationTests.Core.Models.Entities; -using BuilderGenerator.IntegrationTests.Net60.FromPackage.Builders; +using BuilderGenerator.IntegrationTests.Net60.Builders; using NUnit.Framework; using Shouldly; -namespace BuilderGenerator.IntegrationTests.Net60.FromPackage.Tests; +namespace BuilderGenerator.IntegrationTests.Net60.Tests; [TestFixture] public class BuilderTests diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/OrderBuilderTests.cs b/src/BuilderGenerator.IntegrationTests.Net60/Tests/OrderBuilderTests.cs similarity index 92% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/OrderBuilderTests.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Tests/OrderBuilderTests.cs index 4b384e2..4cdb390 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/OrderBuilderTests.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Tests/OrderBuilderTests.cs @@ -1,11 +1,11 @@ using System; using BuilderGenerator.IntegrationTests.Core.Models.Entities; using BuilderGenerator.IntegrationTests.Core.Models.Enums; -using BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; +using BuilderGenerator.IntegrationTests.Net60.Builders; using NUnit.Framework; using Shouldly; -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Tests; +namespace BuilderGenerator.IntegrationTests.Net60.Tests; [TestFixture] public class OrderBuilderTests diff --git a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/UserBuilderTests.cs b/src/BuilderGenerator.IntegrationTests.Net60/Tests/UserBuilderTests.cs similarity index 92% rename from src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/UserBuilderTests.cs rename to src/BuilderGenerator.IntegrationTests.Net60/Tests/UserBuilderTests.cs index 8f7ee56..6b8de27 100644 --- a/src/BuilderGenerator.IntegrationTests.Net60.FromProject/Tests/UserBuilderTests.cs +++ b/src/BuilderGenerator.IntegrationTests.Net60/Tests/UserBuilderTests.cs @@ -1,10 +1,10 @@ using System; using BuilderGenerator.IntegrationTests.Core.Models.Entities; -using BuilderGenerator.IntegrationTests.Net60.FromProject.Builders; +using BuilderGenerator.IntegrationTests.Net60.Builders; using NUnit.Framework; using Shouldly; -namespace BuilderGenerator.IntegrationTests.Net60.FromProject.Tests; +namespace BuilderGenerator.IntegrationTests.Net60.Tests; [TestFixture] public class UserBuilderTests diff --git a/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj b/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj index 4c77637..9ca1ac3 100644 --- a/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj +++ b/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj @@ -7,6 +7,16 @@ true + + + + + + + + + + @@ -17,4 +27,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj.DotSettings b/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj.DotSettings new file mode 100644 index 0000000..3fc2808 --- /dev/null +++ b/src/BuilderGenerator.UnitTests/BuilderGenerator.UnitTests.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/BuilderGenerator.UnitTests/Examples/Input.cs b/src/BuilderGenerator.UnitTests/Examples/Input.cs new file mode 100644 index 0000000..fe361b2 --- /dev/null +++ b/src/BuilderGenerator.UnitTests/Examples/Input.cs @@ -0,0 +1,15 @@ +using BuilderGenerator; + +namespace BuilderGenerator.UnitTests +{ + public class Person + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + [BuilderFor(typeof(Person))] + public partial class PersonBuilder + { + } +} diff --git a/src/BuilderGenerator.UnitTests/Examples/Output.cs b/src/BuilderGenerator.UnitTests/Examples/Output.cs new file mode 100644 index 0000000..fde33cb --- /dev/null +++ b/src/BuilderGenerator.UnitTests/Examples/Output.cs @@ -0,0 +1,68 @@ +#nullable disable + +using System.CodeDom.Compiler; +using BuilderGenerator; + +namespace BuilderGenerator.UnitTests +{ + public partial class PersonBuilder : BuilderGenerator.Builder + { + public System.Lazy FirstName = new System.Lazy(() => default(string)); + public System.Lazy LastName = new System.Lazy(() => default(string)); + + public override BuilderGenerator.UnitTests.Person Build() + { + if (Object?.IsValueCreated != true) + { + Object = new System.Lazy(() => + { + var result = new BuilderGenerator.UnitTests.Person + { + FirstName = FirstName.Value, + LastName = LastName.Value, + }; + + return result; + }); + + PostProcess(Object.Value); + } + + return Object.Value; + } + + public PersonBuilder WithFirstName(string value) + { + return WithFirstName(() => value); + } + + public PersonBuilder WithFirstName(System.Func func) + { + FirstName = new System.Lazy(func); + return this; + } + + public PersonBuilder WithoutFirstName() + { + FirstName = new System.Lazy(() => default(string)); + return this; + } + + public PersonBuilder WithLastName(string value) + { + return WithLastName(() => value); + } + + public PersonBuilder WithLastName(System.Func func) + { + LastName = new System.Lazy(func); + return this; + } + + public PersonBuilder WithoutLastName() + { + LastName = new System.Lazy(() => default(string)); + return this; + } + } +} diff --git a/src/BuilderGenerator.UnitTests/Given_a_BuilderGenerator.cs b/src/BuilderGenerator.UnitTests/Given_a_BuilderGenerator.cs index 4715f9c..6709cc0 100644 --- a/src/BuilderGenerator.UnitTests/Given_a_BuilderGenerator.cs +++ b/src/BuilderGenerator.UnitTests/Given_a_BuilderGenerator.cs @@ -1,3 +1,5 @@ +using System; +using System.IO; using System.Linq; using System.Reflection; using Microsoft.CodeAnalysis; @@ -13,97 +15,49 @@ public class GeneratorTests [Test] public void SimpleGeneratorTest() { - // Create the 'input' compilation that the generator will act on - var inputCompilation = CreateCompilation( - @" -using BuilderGenerator; - -namespace BuilderGenerator.UnitTests -{ - public class Person - { - public string FirstName { get; set; } - } - - [BuilderFor(typeof(Person))] - public partial class PersonBuilder - { - } -} -"); - - var expectedOutput = @"// -using BuilderGenerator; -using System.CodeDom.Compiler; - -#nullable disable - -namespace BuilderGenerator.UnitTests -{ - public partial class PersonBuilder : BuilderGenerator.Builder - { - public System.Lazy FirstName = new System.Lazy(() => default(string)); - - public override BuilderGenerator.UnitTests.Person Build() - { - if (Object?.IsValueCreated != true) - { - Object = new System.Lazy(() => - { - var result = new BuilderGenerator.UnitTests.Person - { - FirstName = FirstName.Value, - }; - - return result; - }); - - PostProcess(Object.Value); - } - - return Object.Value; - } - - public PersonBuilder WithFirstName(string value) - { - return WithFirstName(() => value); - } - - public PersonBuilder WithFirstName(System.Func func) - { - FirstName = new System.Lazy(func); - return this; - } - - public PersonBuilder WithoutFirstName() - { - FirstName = new System.Lazy(() => default(string)); - return this; - } - } -}"; - - var generator = new BuilderGenerator(); - GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + var assembly = GetType().Assembly; + var inputCompilation = CreateCompilation(GetResourceAsString(assembly, "Input.cs")); + var expectedOutput = GetResourceAsString(assembly, "Output.cs"); + GeneratorDriver driver = CSharpGeneratorDriver.Create(new BuilderGenerator()); driver = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics); diagnostics.ShouldBeEmpty(); outputCompilation.SyntaxTrees.Count().ShouldBe(4); - //outputCompilation.GetDiagnostics().ShouldBeEmpty(); - var runResult = driver.GetRunResult(); runResult.Diagnostics.ShouldBeEmpty(); - runResult.GeneratedTrees.Length.ShouldBe(3); // The Builder base class, the BuilderFor attribute, and the generated builder. // TODO: Check for the presence of the Builder base class. // TODO: Check for the presence of the BuilderForAttribute class. var generatorResult = runResult.Results[0]; - generatorResult.Generator.GetGeneratorType().ShouldBe(generator.GetType()); + generatorResult.Generator.GetGeneratorType().ShouldBe(new BuilderGenerator().GetType()); generatorResult.Exception.ShouldBeNull(); generatorResult.GeneratedSources.Length.ShouldBe(3); - generatorResult.GeneratedSources[2].SourceText.ToString().ShouldBe(expectedOutput); + + var sourceText = generatorResult.GeneratedSources[2].SourceText.ToString(); + + // Since the generation time will keep changing, we'll just compare everything after the first instance of the word "using". + sourceText[sourceText.IndexOf("using", StringComparison.OrdinalIgnoreCase)..].ShouldBe(expectedOutput[expectedOutput.IndexOf("using", StringComparison.OrdinalIgnoreCase)..]); + } + + public static string GetResourceAsString(Assembly assembly, string resourceName) + { + var manifestResourceNames = assembly.GetManifestResourceNames(); + resourceName = manifestResourceNames.Single(x => x.Equals($"BuilderGenerator.UnitTests.Examples.{resourceName}", StringComparison.OrdinalIgnoreCase)); + + using (var stream = assembly.GetManifestResourceStream(resourceName)) + { + if (stream == null) + { + throw new InvalidOperationException($"Resource '{resourceName}' not found."); + } + + using (var reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } + } } private static Compilation CreateCompilation(string source) diff --git a/src/BuilderGenerator.UnitTests/Properties/Resources.Designer.cs b/src/BuilderGenerator.UnitTests/Properties/Resources.Designer.cs new file mode 100644 index 0000000..8aa2909 --- /dev/null +++ b/src/BuilderGenerator.UnitTests/Properties/Resources.Designer.cs @@ -0,0 +1,110 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace BuilderGenerator.UnitTests.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BuilderGenerator.UnitTests.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to using BuilderGenerator; + /// + ///namespace BuilderGenerator.UnitTests + ///{ + /// public class Person + /// { + /// public string FirstName { get; set; } + /// public string LastName { get; set; } + /// } + /// + /// [BuilderFor(typeof(Person))] + /// public partial class PersonBuilder + /// { + /// } + ///} + ///. + /// + internal static string Input { + get { + return ResourceManager.GetString("Input", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to #nullable disable + /// + ///using System.CodeDom.Compiler; + ///using BuilderGenerator; + /// + ///namespace BuilderGenerator.UnitTests + ///{ + /// public partial class PersonBuilder : BuilderGenerator.Builder<BuilderGenerator.UnitTests.Person> + /// { + /// public System.Lazy<string> FirstName = new System.Lazy<string>(() => default(string)); + /// public System.Lazy<string> LastName = new System.Lazy<string>(() => default(string)); + /// + /// public override BuilderGenerator.UnitTests.Person Build() + /// { + /// [rest of string was truncated]";. + /// + internal static string Output { + get { + return ResourceManager.GetString("Output", resourceCulture); + } + } + } +} diff --git a/src/BuilderGenerator.UnitTests/Properties/Resources.resx b/src/BuilderGenerator.UnitTests/Properties/Resources.resx new file mode 100644 index 0000000..b298613 --- /dev/null +++ b/src/BuilderGenerator.UnitTests/Properties/Resources.resx @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Examples\Input.cs;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + + ..\Examples\Output.cs;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252 + + \ No newline at end of file diff --git a/src/BuilderGenerator.sln b/src/BuilderGenerator.sln index 4ba5598..96848bf 100644 --- a/src/BuilderGenerator.sln +++ b/src/BuilderGenerator.sln @@ -40,9 +40,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuilderGenerator.UnitTests" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuilderGenerator.IntegrationTests.Core", "BuilderGenerator.IntegrationTests.Core\BuilderGenerator.IntegrationTests.Core.csproj", "{4C0FCA0D-2B74-4125-B106-A32191D39EDB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuilderGenerator.IntegrationTests.Net60.FromPackage", "BuilderGenerator.IntegrationTests.Net60.FromPackage\BuilderGenerator.IntegrationTests.Net60.FromPackage.csproj", "{BD81CADD-0C09-4D3B-83D9-3122E4117085}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuilderGenerator.IntegrationTests.Net60.FromProject", "BuilderGenerator.IntegrationTests.Net60.FromProject\BuilderGenerator.IntegrationTests.Net60.FromProject.csproj", "{8428D6E5-FF2C-423C-AF8E-22841E5CBE15}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuilderGenerator.IntegrationTests.Net60", "BuilderGenerator.IntegrationTests.Net60\BuilderGenerator.IntegrationTests.Net60.csproj", "{5C41CBD1-A607-42B2-8DDE-50061662B5A8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -62,14 +60,10 @@ Global {4C0FCA0D-2B74-4125-B106-A32191D39EDB}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C0FCA0D-2B74-4125-B106-A32191D39EDB}.Release|Any CPU.ActiveCfg = Release|Any CPU {4C0FCA0D-2B74-4125-B106-A32191D39EDB}.Release|Any CPU.Build.0 = Release|Any CPU - {BD81CADD-0C09-4D3B-83D9-3122E4117085}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BD81CADD-0C09-4D3B-83D9-3122E4117085}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BD81CADD-0C09-4D3B-83D9-3122E4117085}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BD81CADD-0C09-4D3B-83D9-3122E4117085}.Release|Any CPU.Build.0 = Release|Any CPU - {8428D6E5-FF2C-423C-AF8E-22841E5CBE15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8428D6E5-FF2C-423C-AF8E-22841E5CBE15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8428D6E5-FF2C-423C-AF8E-22841E5CBE15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8428D6E5-FF2C-423C-AF8E-22841E5CBE15}.Release|Any CPU.Build.0 = Release|Any CPU + {5C41CBD1-A607-42B2-8DDE-50061662B5A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C41CBD1-A607-42B2-8DDE-50061662B5A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C41CBD1-A607-42B2-8DDE-50061662B5A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C41CBD1-A607-42B2-8DDE-50061662B5A8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/BuilderGenerator/Assets/ReadMe.old.md b/src/BuilderGenerator/Assets/ReadMe.old.md deleted file mode 100644 index 803e8f5..0000000 --- a/src/BuilderGenerator/Assets/ReadMe.old.md +++ /dev/null @@ -1,19 +0,0 @@ -# BuilderGenerator # - -This project contains the implementation of the BuilderGenerator class itself, along with various support classes. - -## BuilderGenerator ## - -This class is the brains of the outfit. It reacts to changes in the target project, looking for classes decorated with the BuilderFor attribute, and generating partial builder classes for them. - -## TemplateParser ## - -This is a utility class to do regex-based search and replace over a string template, looking for all tags in a given format, and replacing them with their corresponding values in an internal dictionary of values. Its main difference is that it only needs to make a single pass over the source string. It is used here as a way to set certain values only once, while allowing others, such as property names, to change on each iteration through a loop. - -## Templates ## - -The string templates are gathered together here to keep the BuilderGenerator class small and focused. The hope is to eventually pull these templates from the consuming project itself to allow for customization on a per-project basis. Since the move to .Net 6, the mechanism being used previously no longer works, so I've put this on hold, but hope to return to it. - -## Diagnostics ## - -Diagnostic report classes to allow the generator to complain about things it doesn't like. \ No newline at end of file diff --git a/src/BuilderGenerator/BuilderGenerator.cs b/src/BuilderGenerator/BuilderGenerator.cs index 722f305..0aaf4ac 100644 --- a/src/BuilderGenerator/BuilderGenerator.cs +++ b/src/BuilderGenerator/BuilderGenerator.cs @@ -1,6 +1,8 @@ +#nullable enable + using System; using System.Collections.Generic; -using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -21,6 +23,9 @@ internal class BuilderGenerator : IIncrementalGenerator private static readonly string BuilderForAttribute; private static readonly string BuildMethod; private static readonly string BuildMethodSetter; +#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 WithMethod; @@ -66,83 +71,44 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); // Register generation for classes based on the project contents - var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider(Predicate, Transform).Where(static node => node is not null); - var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); - context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Left, source.Right!, spc)); + var provider = context.SyntaxProvider.CreateSyntaxProvider(Predicate, Transform).Where(static builderInfo => builderInfo is not null).Collect().SelectMany((builders, _) => builders.Distinct()); + context.RegisterSourceOutput(provider, Execute); } - private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) + private static void Execute(SourceProductionContext context, BuilderInfo? builder) { - if (classes.IsDefaultOrEmpty) { return; } + _ = builder ?? throw new ArgumentNullException(nameof(builder)); - var distinctClasses = classes.Distinct(); + var stopwatch = Stopwatch.StartNew(); + var templateParser = new TemplateParser(); - foreach (var typeDeclaration in distinctClasses) + try { - try - { - var semanticModel = compilation.GetSemanticModel(typeDeclaration.SyntaxTree); - var typeSymbol = semanticModel.GetDeclaredSymbol(typeDeclaration, context.CancellationToken); - var templateParser = new TemplateParser(); - - if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) { continue; } - - if (namedTypeSymbol.IsAbstract) - { - context.ReportDiagnostic(DiagnosticDescriptors.TargetClassIsAbstract(typeDeclaration.GetLocation(), typeDeclaration.Identifier.ToString())); - } - - var attributeSymbol = namedTypeSymbol.GetAttributes().SingleOrDefault(x => x.AttributeClass!.Name == nameof(BuilderForAttribute)); - - if (attributeSymbol is null) { continue; } - - var targetClassType = attributeSymbol.ConstructorArguments[0]; - var targetClassName = ((ISymbol)targetClassType.Value!).Name; - var targetClassFullName = targetClassType.Value!.ToString(); - var includeInternals = (bool)attributeSymbol.ConstructorArguments[1].Value!; - - var targetClassProperties = GetPropertySymbols((INamedTypeSymbol)targetClassType.Value, includeInternals) - .Select(x => new ValueTuple(x.Name, x.Type.ToString())) - .Distinct() - .OrderBy(x => x.Name) - .ToList(); - - var builderClassName = typeSymbol.Name; - var builderClassNamespace = typeSymbol.ContainingNamespace.ToString(); - var builderClassUsingBlock = ((CompilationUnitSyntax)typeDeclaration.SyntaxTree.GetRoot()).Usings.ToString(); - var builderClassAccessibility = typeSymbol.DeclaredAccessibility.ToString().ToLower(); - - templateParser.SetTag("GeneratedAt", DateTime.Now.ToString("s")); - templateParser.SetTag("BuilderClassUsingBlock", builderClassUsingBlock); - templateParser.SetTag("BuilderClassNamespace", builderClassNamespace); - templateParser.SetTag("BuilderClassAccessibility", builderClassAccessibility); - templateParser.SetTag("BuilderClassName", builderClassName); - templateParser.SetTag("TargetClassName", targetClassName); - templateParser.SetTag("TargetClassFullName", targetClassFullName); - - var builderClassProperties = GenerateProperties(templateParser, targetClassProperties); - var builderClassBuildMethod = GenerateBuildMethod(templateParser, targetClassProperties); - var builderClassWithMethods = GenerateWithMethods(templateParser, targetClassProperties); - - templateParser.SetTag("Properties", builderClassProperties); - templateParser.SetTag("BuildMethod", builderClassBuildMethod); - templateParser.SetTag("WithMethods", builderClassWithMethods); - - var source = templateParser.ParseString(BuilderClass); - - context.AddSource($"{builderClassName}.g.cs", SourceText.From(source, Encoding.UTF8)); - } - catch (Exception e) - { - context.ReportDiagnostic(DiagnosticDescriptors.UnexpectedErrorDiagnostic(e, typeDeclaration.GetLocation(), typeDeclaration.Identifier.ToString())); - } + templateParser.SetTag("GenerationTime", DateTime.Now.ToString("s")); + templateParser.SetTag("GenerationDuration", (builder.Value.TimeToGenerate + stopwatch.Elapsed).TotalMilliseconds); + templateParser.SetTag("BuilderClassUsingBlock", builder.Value.BuilderClassUsingBlock); + templateParser.SetTag("BuilderClassNamespace", builder.Value.BuilderClassNamespace); + templateParser.SetTag("BuilderClassAccessibility", builder.Value.BuilderClassAccessibility.ToString().ToLower()); + templateParser.SetTag("BuilderClassName", builder.Value.BuilderClassName); + templateParser.SetTag("TargetClassName", builder.Value.TargetClassName); + templateParser.SetTag("TargetClassFullName", builder.Value.TargetClassFullName); + templateParser.SetTag("Properties", GenerateProperties(templateParser, builder.Value.Properties)); + templateParser.SetTag("BuildMethod", GenerateBuildMethod(templateParser, builder.Value.Properties)); + templateParser.SetTag("WithMethods", GenerateWithMethods(templateParser, builder.Value.Properties)); + + var source = templateParser.ParseString(BuilderClass); + context.AddSource($"{builder.Value.BuilderClassName}.g.cs", SourceText.From(source, Encoding.UTF8)); + } + catch (Exception e) + { + context.ReportDiagnostic(DiagnosticDescriptors.UnexpectedErrorDiagnostic(e, builder.Value.Location, builder.Value.Identifier)); } } - private static string GenerateBuildMethod(TemplateParser templateParser, IEnumerable<(string Name, string TypeName)> properties) + private static string GenerateBuildMethod(TemplateParser templateParser, IEnumerable properties) { var setters = string.Join( - "\n", + NewLine, properties.Select( x => { @@ -157,15 +123,15 @@ private static string GenerateBuildMethod(TemplateParser templateParser, IEnumer return result; } - private static string GenerateProperties(TemplateParser templateParser, IEnumerable<(string Name, string TypeName)> properties) + private static string GenerateProperties(TemplateParser templateParser, IEnumerable properties) { var result = string.Join( - "\n", + NewLine, properties.Select( x => { templateParser.SetTag("PropertyName", x.Name); - templateParser.SetTag("PropertyType", x.TypeName); + templateParser.SetTag("PropertyType", x.Type); return templateParser.ParseString(Property); })); @@ -173,15 +139,15 @@ private static string GenerateProperties(TemplateParser templateParser, IEnumera return result; } - private static string GenerateWithMethods(TemplateParser templateParser, IEnumerable<(string Name, string TypeName)> properties) + private static string GenerateWithMethods(TemplateParser templateParser, IEnumerable properties) { var result = string.Join( - "\n", + NewLine, properties.Select( x => { templateParser.SetTag("PropertyName", x.Name); - templateParser.SetTag("PropertyType", x.TypeName); + templateParser.SetTag("PropertyType", x.Type); return templateParser.ParseString(WithMethod); })); @@ -207,25 +173,64 @@ private static IEnumerable GetPropertySymbols(INamedTypeSymbol return symbols; } + /// Performs a first-pass filtering of syntax nodes that might possibly represent a builder class. + /// The syntax node being examined. + /// A cancellation token (currently unused). + /// A indicating whether or not might possibly represent a builder class. private static bool Predicate(SyntaxNode node, CancellationToken _) => node is TypeDeclarationSyntax { AttributeLists.Count: > 0 }; - private static TypeDeclarationSyntax? Transform(GeneratorSyntaxContext context, CancellationToken token) + /// Transforms the syntax node into a containing the information needed to generate a Builder. + /// The , which contains a reference to the node. + /// A cancellation token, used to short-circuit the transformation if additional changes are detected. + /// A describing the Builder if the node represents a builder; otherwise, null. + private static BuilderInfo? Transform(GeneratorSyntaxContext context, CancellationToken token) { + var stopwatch = Stopwatch.StartNew(); var node = context.Node; if (node is not TypeDeclarationSyntax typeNode) { return null; } - var model = context.SemanticModel; - - var typeSymbol = model.GetDeclaredSymbol(typeNode, token); + var typeSymbol = context.SemanticModel.GetDeclaredSymbol(typeNode, token); if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) { return null; } - if (namedTypeSymbol.GetAttributes().Any(x => x.AttributeClass?.Name == "BuilderForAttribute")) + if (!namedTypeSymbol.GetAttributes().Any(x => x.AttributeClass?.Name == "BuilderForAttribute")) { return null; } + + var attributeSymbol = namedTypeSymbol.GetAttributes().SingleOrDefault(x => x.AttributeClass!.Name == nameof(BuilderForAttribute)); + + if (attributeSymbol is null) { return null; } + + // The node represents a Builder class, so we can go ahead and do the transformation now. + // We gather all the relevant information up-front so that it can be used effectively for caching. + var targetClassType = attributeSymbol.ConstructorArguments[0]; + var includeInternals = (bool)attributeSymbol.ConstructorArguments[1].Value!; + + var targetClassProperties = GetPropertySymbols((INamedTypeSymbol)targetClassType.Value!, includeInternals) + .Select(x => new ValueTuple(x.Name, x.Type.ToString(), x.DeclaredAccessibility)) + .Distinct() + .OrderBy(x => x.Name) + .ToList(); + + var result = new BuilderInfo { - return typeNode; - } + BuilderClassAccessibility = typeSymbol.DeclaredAccessibility, + BuilderClassNamespace = typeSymbol.ContainingNamespace.ToString(), + BuilderClassName = typeSymbol.Name, + TargetClassName = ((ISymbol)targetClassType.Value!).Name, + TargetClassFullName = targetClassType.Value!.ToString(), + BuilderClassUsingBlock = ((CompilationUnitSyntax)typeNode.SyntaxTree.GetRoot()).Usings.ToString(), + Properties = targetClassProperties.Select( + x => new PropertyInfo + { + Accessibility = x.Accessibility, + Name = x.Name, + Type = x.TypeName, + }).ToList(), + Location = typeNode.GetLocation(), + Identifier = typeNode.Identifier.ToString(), + TimeToGenerate = stopwatch.Elapsed, + }; - return null; + return result; } } diff --git a/src/BuilderGenerator/BuilderGenerator.csproj b/src/BuilderGenerator/BuilderGenerator.csproj index c254781..f2bb486 100644 --- a/src/BuilderGenerator/BuilderGenerator.csproj +++ b/src/BuilderGenerator/BuilderGenerator.csproj @@ -7,10 +7,9 @@ 2021-2023 Mel Grubb Generates builder classes for testing and/or seed data. Please see documentation site (https://melgrubb.github.io/BuilderGenerator/) for complete details. true - false true en - 2.2.0 + 2.3.0 $(Version).0 $(Version).0 $(Version).0 @@ -18,13 +17,18 @@ true + false BuilderGenerator logo.png LICENSE.txt https://github.com/MelGrubb/BuilderGenerator README.md BDD;TDD;Testing;Builders;Code Generation;Source Generators - + v2.3.0 + - Major caching and performance improvements + - Internal code cleanup + - Conversion of templates to embedded resources + v2.2.0 - Changed generated file extension to .g.cs @@ -86,6 +90,14 @@ https://github.com/MelGrubb/BuilderGenerator + + 1701;1702;NU5128 + + + + 1701;1702;NU5128 + + diff --git a/src/BuilderGenerator/BuilderInfo.cs b/src/BuilderGenerator/BuilderInfo.cs new file mode 100644 index 0000000..c9530b7 --- /dev/null +++ b/src/BuilderGenerator/BuilderInfo.cs @@ -0,0 +1,61 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace BuilderGenerator; + +internal record struct BuilderInfo +{ + public Accessibility BuilderClassAccessibility { get; set; } + public string BuilderClassName { get; set; } + public string BuilderClassNamespace { get; set; } + public string BuilderClassUsingBlock { get; set; } + public string Identifier { get; set; } + public Location Location { get; set; } + public List Properties { get; set; } + public string TargetClassFullName { get; set; } + public string TargetClassName { get; set; } + + /// Gets or sets the time it took to generate a particular builder. + /// The time to generate the builder class. + /// Note that this property is not included in the hash. + public TimeSpan TimeToGenerate { get; set; } + + public bool Equals(BuilderInfo other) + { + var result = + BuilderClassAccessibility == other.BuilderClassAccessibility + && BuilderClassName == other.BuilderClassName + && BuilderClassNamespace == other.BuilderClassNamespace + && BuilderClassUsingBlock == other.BuilderClassUsingBlock + && Identifier == other.Identifier + && Location == other.Location + && TargetClassFullName == other.TargetClassFullName + && TargetClassName == other.TargetClassName + && Properties.SequenceEqual(other.Properties); + + return result; + } + + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = (hash * 23) + BuilderClassAccessibility.GetHashCode(); + hash = (hash * 23) + BuilderClassName.GetHashCode(); + hash = (hash * 23) + BuilderClassNamespace.GetHashCode(); + hash = (hash * 23) + BuilderClassUsingBlock.GetHashCode(); + hash = (hash * 23) + Identifier.GetHashCode(); + hash = (hash * 23) + Location.GetHashCode(); + hash = (hash * 23) + TargetClassFullName.GetHashCode(); + hash = (hash * 23) + TargetClassName.GetHashCode(); + hash = (hash * 23) + Properties.Aggregate(hash, (current, property) => (current * 23) + property.GetHashCode()); + + return hash; + } + } +} diff --git a/src/BuilderGenerator/PropertyInfo.cs b/src/BuilderGenerator/PropertyInfo.cs new file mode 100644 index 0000000..a6859a9 --- /dev/null +++ b/src/BuilderGenerator/PropertyInfo.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; + +namespace BuilderGenerator; + +internal record struct PropertyInfo +{ + /// Gets or sets the accessibility of the target class property. + /// The accessibility of the target class property. + /// Although this isn't used by the templates themselves, a change in accessibility could result in a re-generation of the builder, so we need this to be part of the hash code. + public Accessibility Accessibility { get; set; } + + /// Gets or sets the name of the target class property. + /// The name of the target class property. + public string Name { get; set; } + + public string Type { get; set; } +} diff --git a/src/BuilderGenerator/Templates/BuildMethod.txt b/src/BuilderGenerator/Templates/BuildMethod.txt index e612daf..e7edd4d 100644 --- a/src/BuilderGenerator/Templates/BuildMethod.txt +++ b/src/BuilderGenerator/Templates/BuildMethod.txt @@ -1,12 +1,13 @@ + public override {{TargetClassFullName}} Build() { if (Object?.IsValueCreated != true) { - Object = new System.Lazy<{{TargetClassFullName}}>(() => + Object = new System.Lazy<{{TargetClassFullName}}>(() => { - var result = new {{TargetClassFullName}} + var result = new {{TargetClassFullName}} { - {{Setters}} +{{Setters}} }; return result; diff --git a/src/BuilderGenerator/Templates/BuildMethodSetter.txt b/src/BuilderGenerator/Templates/BuildMethodSetter.txt index c58f18f..71ab8ad 100644 --- a/src/BuilderGenerator/Templates/BuildMethodSetter.txt +++ b/src/BuilderGenerator/Templates/BuildMethodSetter.txt @@ -1 +1 @@ - {{PropertyName}} = {{PropertyName}}.Value, \ No newline at end of file + {{PropertyName}} = {{PropertyName}}.Value, \ No newline at end of file diff --git a/src/BuilderGenerator/Templates/BuilderClass.txt b/src/BuilderGenerator/Templates/BuilderClass.txt index 8c400f8..ca81efd 100644 --- a/src/BuilderGenerator/Templates/BuilderClass.txt +++ b/src/BuilderGenerator/Templates/BuilderClass.txt @@ -1,8 +1,12 @@ +#nullable disable + +//------------------------------------------------------------------------------ // -{{BuilderClassUsingBlock}} +// This code was generated by BuilderGenerator at {{GenerationTime:u}} in {{GenerationDuration}}ms. +// +//------------------------------------------------------------------------------ using System.CodeDom.Compiler; - -#nullable disable +{{BuilderClassUsingBlock}} namespace {{BuilderClassNamespace}} { @@ -12,4 +16,4 @@ namespace {{BuilderClassNamespace}} {{BuildMethod}} {{WithMethods}} } -} \ No newline at end of file +} diff --git a/src/BuilderGenerator/Templates/Property.txt b/src/BuilderGenerator/Templates/Property.txt index fd6f719..bc993e1 100644 --- a/src/BuilderGenerator/Templates/Property.txt +++ b/src/BuilderGenerator/Templates/Property.txt @@ -1 +1 @@ - public System.Lazy<{{PropertyType}}> {{PropertyName}} = new System.Lazy<{{PropertyType}}>(() => default({{PropertyType}})); + public System.Lazy<{{PropertyType}}> {{PropertyName}} = new System.Lazy<{{PropertyType}}>(() => default({{PropertyType}})); \ No newline at end of file diff --git a/src/BuilderGenerator/Templates/WithMethod.txt b/src/BuilderGenerator/Templates/WithMethod.txt index d11ef60..e1b81ed 100644 --- a/src/BuilderGenerator/Templates/WithMethod.txt +++ b/src/BuilderGenerator/Templates/WithMethod.txt @@ -11,7 +11,7 @@ } public {{BuilderClassName}} Without{{PropertyName}}() - { + { {{PropertyName}} = new System.Lazy<{{PropertyType}}>(() => default({{PropertyType}})); return this; } \ No newline at end of file diff --git a/src/Publish-Local.ps1 b/src/Publish-Local.ps1 index bd5eddf..e3f8ea5 100644 --- a/src/Publish-Local.ps1 +++ b/src/Publish-Local.ps1 @@ -1,21 +1,7 @@ -Write-Output "Synchronizing Assembly Version Info" [xml]$buildProps = Get-Content BuilderGenerator\BuilderGenerator.csproj $version = $buildProps.Project.PropertyGroup.Version -$buildProps.Project.PropertyGroup.AssemblyVersion = $version -$buildProps.Project.PropertyGroup.FileVersion = $version -$buildProps.Save((Resolve-Path "BuilderGenerator\BuilderGenerator.csproj")) +$version = "$version".Trim() -Write-Output "Synchronizing Package Version Info" -[xml]$packageProps = Get-Content BuilderGenerator.Package\BuilderGenerator.Package.csproj -$packageProps.Project.PropertyGroup[1].PackageVersion = $version -$packageProps.Save((Resolve-Path "BuilderGenerator.Package\BuilderGenerator.Package.csproj")) - -#Write-Output "Building version $version" -#dotnet build .\BuilderGenerator\BuilderGenerator.csproj --configuration debug --verbosity minimal - -Write-Output "Building Package Version $version" -dotnet build .\BuilderGenerator.Package\BuilderGenerator.Package.csproj --configuration debug --verbosity minimal - -Write-Output "Publishing package to local NuGet repo" -$path = '.\BuilderGenerator.Package\bin\Debug\BuilderGenerator.' + $version + '.nupkg' -dotnet nuget push $path --source 'C:\Projects\NuGet Local Repo' +Write-Output "Publishing package version '$version' to local NuGet repo" +$path = '.\BuilderGenerator\bin\Debug\BuilderGenerator.' + $version + '.nupkg' +dotnet nuget push $path --source 'C:\NuGet\BuilderGenerator'