Skip to content

Commit

Permalink
If a propert has a default set, use that in the builder (#45)
Browse files Browse the repository at this point in the history
* wip

* .

* .

* usings

* .

* .

* extra

* refactor

* ...

* ai

* updatedText

* .

* .
  • Loading branch information
StefH authored Jul 12, 2022
1 parent e780fc6 commit f6d08b2
Show file tree
Hide file tree
Showing 16 changed files with 402 additions and 71 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Or via the Visual Studio NuGet package manager or if you use the `dotnet` comman
### Annotate a class
Annotate an existing class with `[FluentBuilder.AutoGenerateBuilder]` to indicate that a FluentBuilder should be generated for this class:
``` c#
using FluentBuilder;

[AutoGenerateBuilder]
public class User
{
Expand All @@ -33,12 +35,18 @@ public class User
public string LastName { get; set; }

public DateTime? Date { get; set; }

[FluentBuilderIgnore] // Add this attribute to ignore this property when generating a FluentBuilder.
public int Age { get; set; }

public int Answer { get; set; } = 42; // When a default value is set, this value is also set as default in the FluentBuilder.
}
```

### Use FluentBuilder
``` c#
using System;
using FluentBuilder;

namespace Test
{
Expand All @@ -59,6 +67,8 @@ namespace Test

### Use FluentBuilder when the class has a default constructor
``` c#
using FluentBuilder;

[AutoGenerateBuilder]
public class User
{
Expand All @@ -77,6 +87,7 @@ public class User

``` c#
using System;
using FluentBuilder;

namespace Test
{
Expand All @@ -96,6 +107,8 @@ namespace Test

### Using FluentBuilder when a class has an `Array` or `IEnumerable<T>` property
``` c#
using FluentBuilder;

[AutoGenerateBuilder]
public class UserDto
{
Expand Down Expand Up @@ -129,6 +142,8 @@ var user = new UserDtoBuilder()

### Using FluentBuilder when a class has an `IDictionary<TKey, TValue>` property
``` c#
using FluentBuilder;

[AutoGenerateBuilder]
public class UserDto
{
Expand All @@ -153,6 +168,8 @@ This scenario is very usefull when you cannot modify the class to annotate it.
### Create a public and partial builder class
And annotate this class with `[AutoGenerateBuilder(typeof(XXX))]` where `XXX` is the type for which you want to generate a FluentBuilder.
``` c#
using FluentBuilder;

[AutoGenerateBuilder(typeof(UserDto))]
public partial class MyUserDtoBuilder
{
Expand All @@ -162,6 +179,7 @@ public partial class MyUserDtoBuilder
### Use FluentBuilder
``` c#
using System;
using FluentBuilder;

namespace Test
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentBuilder" Version="0.4.3">
<PackageReference Include="FluentBuilder" Version="0.4.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</ItemGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Release' ">
<PackageReference Include="FluentBuilder" Version="0.4.3">
<PackageReference Include="FluentBuilder" Version="0.4.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ private static SyntaxTree GetSyntaxTree(SourceFile source)
}

// https://stackoverflow.com/questions/21754908/cant-update-changes-to-tree-roslyn
return CSharpSyntaxTree.ParseText(rootSyntaxNode.GetText().ToString(), null, source.Path);
var updatedText = rootSyntaxNode.GetText().ToString();
return CSharpSyntaxTree.ParseText(updatedText, null, source.Path);
}

private static SyntaxNode AddExtraAttribute<T>(SyntaxTree syntaxTree, AnyOf<string, ExtraAttribute> attributeToAdd)
Expand Down
33 changes: 33 additions & 0 deletions src/FluentBuilderGenerator/Extensions/PropertySymbolExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FluentBuilderGenerator.Types;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace FluentBuilderGenerator.Extensions;

Expand All @@ -18,6 +19,38 @@ internal static bool IsSettable(this IPropertySymbol property)
return property.SetMethod is { IsInitOnly: false };
}

/// <summary>
/// Check if the <see cref="IPropertySymbol"/> has a value set, in that case try to get that value and return the usings.
/// If no value is set, just return the default value.
/// </summary>
internal static (string DefaultValue, IReadOnlyList<string>? ExtraUsings) GetDefaultValue(this IPropertySymbol property)
{
var location = property.Locations.FirstOrDefault();
if (location != null)
{
var rootSyntaxNode = location.SourceTree?.GetRoot();
if (rootSyntaxNode != null)
{
var propertyDeclarationSyntax = rootSyntaxNode.FindDescendantNode<PropertyDeclarationSyntax>(p => p.Identifier.ValueText == property.Name);

if (propertyDeclarationSyntax is { Initializer: { } })
{
var thisUsings = rootSyntaxNode.FindDescendantNodes<UsingDirectiveSyntax>().Select(ud => ud.Name.ToString());

var ancestorUsings = rootSyntaxNode.GetAncestorsUsings().Select(ud => ud.Name.ToString());

var extraUsings = thisUsings.Union(ancestorUsings).Distinct().ToList();

var value = propertyDeclarationSyntax.Initializer.Value.ToString();

return (value, extraUsings);
}
}
}

return (property.Type.GetDefault(), null);
}

internal static bool TryGetIDictionaryElementTypes(this IPropertySymbol property, out (INamedTypeSymbol key, INamedTypeSymbol value)? tuple)
{
var type = property.Type.GetFluentTypeKind();
Expand Down
2 changes: 1 addition & 1 deletion src/FluentBuilderGenerator/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace FluentBuilderGenerator.Extensions;

internal static class StringExtensions
{
private static readonly Regex ExtractValueBetween = new Regex("(?<=<).*(?=>)", RegexOptions.Compiled);
private static readonly Regex ExtractValueBetween = new("(?<=<).*(?=>)", RegexOptions.Compiled);

public static bool TryGetGenericTypeArguments(this string input, [NotNullWhen(true)] out string? genericTypeArgumentValue)
{
Expand Down
29 changes: 18 additions & 11 deletions src/FluentBuilderGenerator/Extensions/SyntaxNodeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using FluentBuilderGenerator.Models;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Expand Down Expand Up @@ -93,22 +92,30 @@ public static bool TryGetParentSyntax<T>(this SyntaxNode? syntaxNode, [NotNullWh
}

// https://stackoverflow.com/questions/49970813/collect-usings-from-all-enclosing-namespaces-having-an-itypesymbol
public static IReadOnlyList<UsingDirectiveSyntax> GetAllUsings(this SyntaxNode syntaxNode)
public static IReadOnlyList<UsingDirectiveSyntax> GetAncestorsUsings(this SyntaxNode syntaxNode)
{
var allUsings = SyntaxFactory.List<UsingDirectiveSyntax>();

foreach (var parent in syntaxNode.Ancestors(false))
{
if (parent is NamespaceDeclarationSyntax namespaceDeclarationSyntax)
{
allUsings = allUsings.AddRange(namespaceDeclarationSyntax.Usings);
}
else if (parent is CompilationUnitSyntax compilationUnitSyntax)
allUsings = parent switch
{
allUsings = allUsings.AddRange(compilationUnitSyntax.Usings);
}
NamespaceDeclarationSyntax namespaceDeclarationSyntax => allUsings.AddRange(namespaceDeclarationSyntax.Usings),
CompilationUnitSyntax compilationUnitSyntax => allUsings.AddRange(compilationUnitSyntax.Usings),
_ => allUsings
};
}

return allUsings;
}

public static IReadOnlyList<T> FindDescendantNodes<T>(this SyntaxNode syntaxNode, Func<T, bool>? predicate = null) where T : SyntaxNode
{
return syntaxNode.DescendantNodes().OfType<T>().Where(x => predicate == null || predicate(x)).ToList();
}

public static T? FindDescendantNode<T>(this SyntaxNode syntaxNode, Func<T, bool>? predicate = null) where T : SyntaxNode
{
return syntaxNode.DescendantNodes().OfType<T>().FirstOrDefault(x => predicate == null || predicate(x));
}
}
20 changes: 6 additions & 14 deletions src/FluentBuilderGenerator/Extensions/TypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public static bool CanSupportCollectionInitializer(this ITypeSymbol typeSymbol)
return false;
}

private static bool HasAddMethod(ITypeSymbol typeSymbol)
private static bool HasAddMethod(INamespaceOrTypeSymbol typeSymbol)
{
return typeSymbol
.GetMembers(WellKnownMemberNames.CollectionInitializerAddMethodName)
Expand Down Expand Up @@ -139,18 +139,14 @@ internal static string GetDefault(this ITypeSymbol typeSymbol)
return $"new {namedTypeSymbol.TypeArguments[0]}[0]";

case FluentTypeKind.ReadOnlyCollection:
{
var listSymbol = (INamedTypeSymbol)typeSymbol;
return $"new {typeSymbol}(new List<{listSymbol.TypeArguments[0]}>())";
}
var readOnlyCollectionSymbol = (INamedTypeSymbol)typeSymbol;
return $"new {typeSymbol}(new List<{readOnlyCollectionSymbol.TypeArguments[0]}>())";

case FluentTypeKind.IList:
case FluentTypeKind.ICollection:
case FluentTypeKind.IReadOnlyCollection:
{
var listSymbol = (INamedTypeSymbol)typeSymbol;
return $"new List<{listSymbol.TypeArguments[0]}>()";
}
var listSymbol = (INamedTypeSymbol)typeSymbol;
return $"new List<{listSymbol.TypeArguments[0]}>()";

case FluentTypeKind.IDictionary:
var dictionarySymbol = (INamedTypeSymbol)typeSymbol;
Expand Down Expand Up @@ -207,11 +203,7 @@ private static string GetNewConstructor(ITypeSymbol typeSymbol)

var bestMatchingConstructor = publicConstructorsWithMatch.OrderByDescending(x => x.Match).First().PublicConstructor;

var constructorParameters = new List<string>();
foreach (var parameter in bestMatchingConstructor.Parameters)
{
constructorParameters.Add(parameter.Type.GetDefault());
}
var constructorParameters = bestMatchingConstructor.Parameters.Select(parameter => parameter.Type.GetDefault());

return $"new {typeSymbol}({string.Join(", ", constructorParameters)})";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ internal partial class FluentBuilderClassesGenerator : IFilesGenerator
{
private const string FluentBuilderIgnoreAttributeClassName = "FluentBuilder.FluentBuilderIgnoreAttribute";

private static readonly string[] SystemUsings =
{
"System",
"System.Collections",
"System.Collections.Generic"
};

private static readonly FileDataType[] ExtraBuilders =
{
FileDataType.ArrayBuilder,
Expand Down Expand Up @@ -58,6 +65,15 @@ public IReadOnlyList<FileData> GenerateFiles()

private string CreateClassBuilderCode(ClassSymbol classSymbol, List<ClassSymbol> allClassSymbols)
{
var property = GenerateWithPropertyCode(classSymbol, allClassSymbols);

var usings = SystemUsings.ToList();
usings.Add($"{_context.AssemblyName}.FluentBuilder");
usings.Add($"{classSymbol.NamedTypeSymbol.ContainingNamespace}");
usings.AddRange(property.ExtraUsings);

var usingsAsStrings = string.Join("\r\n", usings.Distinct().Select(u => $"using {u};"));

return $@"//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by https://github.com/StefH/FluentBuilder version {System.Reflection.Assembly.GetExecutingAssembly().GetName().Version}
Expand All @@ -68,38 +84,42 @@ private string CreateClassBuilderCode(ClassSymbol classSymbol, List<ClassSymbol>
//------------------------------------------------------------------------------
{(_context.SupportsNullable ? "#nullable enable" : string.Empty)}
using System;
using System.Collections;
using System.Collections.Generic;
using {_context.AssemblyName}.FluentBuilder;
using {classSymbol.NamedTypeSymbol.ContainingNamespace};
{usingsAsStrings}
namespace {classSymbol.BuilderNamespace}
{{
public partial class {classSymbol.BuilderClassName} : Builder<{classSymbol.NamedTypeSymbol}>{classSymbol.NamedTypeSymbol.GetWhereStatement()}
{{
{GenerateWithPropertyCode(classSymbol, allClassSymbols)}
{property.StringBuilder}
{GenerateBuildMethod(classSymbol)}
}}
}}
{(_context.SupportsNullable ? "#nullable disable" : string.Empty)}";
}

private StringBuilder GenerateWithPropertyCode(ClassSymbol classSymbol, List<ClassSymbol> allClassSymbols)
private (StringBuilder StringBuilder, IReadOnlyList<string> ExtraUsings) GenerateWithPropertyCode(ClassSymbol classSymbol, List<ClassSymbol> allClassSymbols)
{
var className = classSymbol.BuilderClassName;

var properties = GetProperties(classSymbol);

var className = classSymbol.BuilderClassName;
var extraUsings = new List<string>();

var sb = new StringBuilder();
foreach (var property in properties)
{
// Use "params" in case it's an Array, else just use type-T.
var type = property.Type.GetFluentTypeKind() == FluentTypeKind.Array ? $"params {property.Type}" : property.Type.ToString();

var (defaultValue, extraUsing) = property.GetDefaultValue();
if (extraUsing != null)
{
extraUsings.AddRange(extraUsing);
}

sb.AppendLine($" private bool _{CamelCase(property.Name)}IsSet;");

sb.AppendLine($" private Lazy<{property.Type}> _{CamelCase(property.Name)} = new Lazy<{property.Type}>(() => {property.Type.GetDefault()});");
sb.AppendLine($" private Lazy<{property.Type}> _{CamelCase(property.Name)} = new Lazy<{property.Type}>(() => {defaultValue});");

sb.AppendLine($" public {className} With{property.Name}({type} value) => With{property.Name}(() => value);");

Expand All @@ -109,14 +129,14 @@ private StringBuilder GenerateWithPropertyCode(ClassSymbol classSymbol, List<Cla

sb.AppendLine($" public {className} Without{property.Name}()");
sb.AppendLine(" {");
sb.AppendLine($" With{property.Name}(() => {property.Type.GetDefault()});");
sb.AppendLine($" With{property.Name}(() => {defaultValue});");
sb.AppendLine($" _{CamelCase(property.Name)}IsSet = false;");
sb.AppendLine(" return this;");
sb.AppendLine(" }");
sb.AppendLine();
}

return sb;
return (sb, extraUsings.Distinct().ToList());
}

private StringBuilder GeneratePropertyActionMethodIfApplicable(
Expand Down Expand Up @@ -226,7 +246,7 @@ private StringBuilder GenerateWithIEnumerableBuilderActionMethod(
.AppendLine(" });");
}

private static IEnumerable<IPropertySymbol> GetProperties(ClassSymbol classSymbol)
private static IReadOnlyList<IPropertySymbol> GetProperties(ClassSymbol classSymbol)
{
var properties = classSymbol.NamedTypeSymbol.GetMembers().OfType<IPropertySymbol>()
.Where(x =>
Expand Down Expand Up @@ -260,7 +280,7 @@ private static string GenerateBuildMethod(ClassSymbol classSymbol)
throw new NotSupportedException($"Unable to generate a FluentBuilder for the class '{classSymbol.NamedTypeSymbol}' because no public parameterless constructor was defined.");
}

var properties = GetProperties(classSymbol).ToArray();
var properties = GetProperties(classSymbol);
// var propertiesInitOnly = properties.Where(property => property.SetMethod!.IsInitOnly).ToArray();
var propertiesSettable = properties.Where(property => property.IsSettable()).ToArray();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private static bool TryGet(ClassDeclarationSyntax classDeclarationSyntax, out Fl
}

// https://github.com/StefH/FluentBuilder/issues/36
usings.AddRange(classDeclarationSyntax.GetAllUsings().Select(@using => @using.Name.ToString()));
usings.AddRange(classDeclarationSyntax.GetAncestorsUsings().Select(@using => @using.Name.ToString()));

usings = usings.Distinct().ToList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ internal interface IGeneratorExecutionContextWrapper

/// <see cref="GeneratorExecutionContext.AddSource(string, SourceText)"/>
public void AddSource(string hintName, SourceText sourceText);

bool TryGetNamedTypeSymbolByFullMetadataName(FluentData fluentDataItem, [NotNullWhen(true)] out ClassSymbol? classSymbol);
}
Loading

0 comments on commit f6d08b2

Please sign in to comment.