diff --git a/README.md b/README.md index ec49f36..140f71d 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ PM> Install-Package M31.FluentApi A package reference will be added to your `csproj` file. Moreover, since this library provides code via source code generation, consumers of your project don't need the reference to `M31.FluentApi`. Therefore, it is recommended to use the `PrivateAssets` metadata tag: ```xml - + ``` If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file: diff --git a/src/ExampleProject/Employee.cs b/src/ExampleProject/Employee.cs new file mode 100644 index 0000000..a1483ed --- /dev/null +++ b/src/ExampleProject/Employee.cs @@ -0,0 +1,42 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +// Example from https://stackoverflow.com/questions/59021513/using-fluent-interface-with-builder-pattern. + +using M31.FluentApi.Attributes; + +namespace ExampleProject; + +[FluentApi] +public class Employee +{ + [FluentMember(0)] + public string Name { get; private set; } + + [FluentCollection(1, "Phone")] + public List Phones { get; private set; } + + [FluentCollection(2, "Job")] + public List Jobs { get; private set; } +} + +[FluentApi] +public class Phone +{ + [FluentMember(0)] + public string Number { get; private set; } + + [FluentMember(1)] + public string Usage { get; private set; } +} + +[FluentApi] +public class Job +{ + [FluentMember(0)] + public string CompanyName { get; private set; } + + [FluentMember(1)] + public int Salary { get; private set; } +} \ No newline at end of file diff --git a/src/ExampleProject/HttpRequest.cs b/src/ExampleProject/HttpRequest.cs new file mode 100644 index 0000000..3bf4235 --- /dev/null +++ b/src/ExampleProject/HttpRequest.cs @@ -0,0 +1,45 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +// Example from https://github.com/dotnet/csharplang/discussions/7325. + +using System.Net.Http.Json; +using System.Text.Json; +using M31.FluentApi.Attributes; + +namespace ExampleProject; + +[FluentApi] +public class HttpRequest +{ + [FluentMember(0)] + public HttpMethod Method { get; private set; } + + [FluentMember(1)] + public string Url { get; private set; } + + [FluentCollection(2, "Header")] + public List<(string, string)> Headers { get; private set; } + + [FluentMember(3)] + public HttpContent Content { get; private set; } + + [FluentMethod(3)] + public void WithJsonContent(T body, Action? configureSerializer = null) + { + JsonSerializerOptions options = new JsonSerializerOptions(JsonSerializerDefaults.Web); + configureSerializer?.Invoke(options); + Content = new StringContent(JsonSerializer.Serialize(body)); + } + + [FluentMethod(4)] + [FluentReturn] + public HttpRequestMessage GetMessage() + { + HttpRequestMessage request = new HttpRequestMessage(Method, Url); + request.Content = Content; + Headers.ForEach(h => request.Headers.Add(h.Item1, h.Item2)); + return request; + } +} \ No newline at end of file diff --git a/src/ExampleProject/Program.cs b/src/ExampleProject/Program.cs index c21b096..645728c 100644 --- a/src/ExampleProject/Program.cs +++ b/src/ExampleProject/Program.cs @@ -67,19 +67,6 @@ Console.WriteLine(hashCode); -// Node -// - -Node tree = CreateTree.Root(8) - .Left(3, n => n - .Left(1) - .Right(6)) - .Right(10, n => n - .LeftNull() - .Right(14)); - -Console.WriteLine(JsonSerializer.Serialize(tree)); - // Docker file // // Example from https://mitesh1612.github.io/blog/2021/08/11/how-to-design-fluent-api. @@ -94,4 +81,48 @@ .WithCommand("npm start") .ToString(); -Console.WriteLine(dockerFile); \ No newline at end of file +Console.WriteLine(dockerFile); + +// Employee +// +// Example from https://stackoverflow.com/questions/59021513/using-fluent-interface-with-builder-pattern. +// + +Employee employee = CreateEmployee + .WithName("My Name") + .WithPhone( + p => p.WithNumber("222-222-2222").WithUsage("CELL")) + .WithJobs( + j => j.WithCompanyName("First Company").WithSalary(100), + j => j.WithCompanyName("Second Company").WithSalary(200)); + +Console.WriteLine(JsonSerializer.Serialize(employee)); + +// HttpRequest +// +// Example from https://github.com/dotnet/csharplang/discussions/7325. +// + +HttpRequestMessage message = CreateHttpRequest + .WithMethod(HttpMethod.Post) + .WithUrl("https://example.com") + .WithHeaders(("Accept", "application/json"), ("Authorization", "Bearer x")) + .WithJsonContent( + new { Name = "X", Quantity = 10 }, + opt => opt.PropertyNameCaseInsensitive = true) + .GetMessage(); + +Console.WriteLine(JsonSerializer.Serialize(message)); + +// Node +// + +Node tree = CreateTree.Root(8) + .Left(3, n => n + .Left(1) + .Right(6)) + .Right(10, n => n + .LeftNull() + .Right(14)); + +Console.WriteLine(JsonSerializer.Serialize(tree)); \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md b/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md index 7e46def..0161e27 100644 --- a/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md +++ b/src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md @@ -68,4 +68,19 @@ M31FA007 | M31.Usage | Error | Partial types are not supported Rule ID | Category | Severity | Notes --------|----------|----------|------- -M31FA023 | M31.Usage | Error | Last builder step cannot be skipped \ No newline at end of file +M31FA023 | M31.Usage | Error | Last builder step cannot be skipped + + +## Release 1.8.0 + +### Removed Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +M31FA011 | M31.Usage | Error | Default constructor is missing + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +M31FA024 | M31.Usage | Error | Constructors are ambiguous \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/ConstructorGenerator.cs b/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/ConstructorGenerator.cs index 3365c8c..846a280 100644 --- a/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/ConstructorGenerator.cs +++ b/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/ConstructorGenerator.cs @@ -18,20 +18,41 @@ public void Modify(CodeBoard codeBoard) } Method constructor = CreateConstructor(codeBoard.Info.BuilderClassName); + int nofParameters = codeBoard.Info.FluentApiTypeConstructorInfo.NumberOfParameters; - if (codeBoard.Info.FluentApiTypeHasPrivateConstructor) + if (codeBoard.Info.FluentApiTypeConstructorInfo.ConstructorIsNonPublic) { - // student = (Student) Activator.CreateInstance(typeof(Student), true)!; - constructor.AppendBodyLine( - $"{instanceName} = ({classNameWithTypeParameters}) " + - $"Activator.CreateInstance(typeof({classNameWithTypeParameters}), true)!;"); + if (nofParameters == 0) + { + // student = (Student) Activator.CreateInstance(typeof(Student), true)!; + constructor.AppendBodyLine( + $"{instanceName} = ({classNameWithTypeParameters}) " + + $"Activator.CreateInstance(typeof({classNameWithTypeParameters}), true)!;"); - codeBoard.CodeFile.AddUsing("System"); + codeBoard.CodeFile.AddUsing("System"); + } + else + { + // student = (Student) Activator.CreateInstance(typeof(Student), BindingFlags.Instance | + // BindingFlags.NonPublic, null, new object?[] { null, null }, null)!; + string parameters = + $"new object?[] {{ {string.Join(", ", Enumerable.Repeat("null", nofParameters))} }}"; + + constructor.AppendBodyLine( + $"{instanceName} = ({classNameWithTypeParameters}) " + + $"Activator.CreateInstance(" + + $"typeof({classNameWithTypeParameters}), BindingFlags.Instance | BindingFlags.NonPublic, null, {parameters}, null)!;"); + + codeBoard.CodeFile.AddUsing("System.Reflection"); + codeBoard.CodeFile.AddUsing("System"); + } } else { - // student = new Student(); - constructor.AppendBodyLine($"{instanceName} = new {classNameWithTypeParameters}();"); + // student = new Student(default!, default!); + string parameters = string.Join(", ", + Enumerable.Repeat("default!", nofParameters)); + constructor.AppendBodyLine($"{instanceName} = new {classNameWithTypeParameters}({parameters});"); } codeBoard.Constructor = constructor; diff --git a/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/BuilderAndTargetInfo.cs b/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/BuilderAndTargetInfo.cs index 8bd0a75..7db5996 100644 --- a/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/BuilderAndTargetInfo.cs +++ b/src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/BuilderAndTargetInfo.cs @@ -1,4 +1,5 @@ using M31.FluentApi.Generator.Commons; +using M31.FluentApi.Generator.SourceGenerators; using M31.FluentApi.Generator.SourceGenerators.Generics; namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardElements; @@ -11,7 +12,7 @@ internal BuilderAndTargetInfo( GenericInfo? genericInfo, bool fluentApiTypeIsStruct, bool fluentApiTypeIsInternal, - bool fluentApiTypeHasPrivateConstructor, + ConstructorInfo fluentApiTypeConstructorInfo, string builderClassName) { Namespace = @namespace; @@ -21,7 +22,7 @@ internal BuilderAndTargetInfo( FluentApiTypeIsStruct = fluentApiTypeIsStruct; FluentApiTypeIsInternal = fluentApiTypeIsInternal; DefaultAccessModifier = fluentApiTypeIsInternal ? "internal" : "public"; - FluentApiTypeHasPrivateConstructor = fluentApiTypeHasPrivateConstructor; + FluentApiTypeConstructorInfo = fluentApiTypeConstructorInfo; BuilderClassName = builderClassName; BuilderClassNameWithTypeParameters = WithTypeParameters(builderClassName, genericInfo); BuilderInstanceName = builderClassName.FirstCharToLower(); @@ -36,7 +37,7 @@ internal BuilderAndTargetInfo( internal bool FluentApiTypeIsStruct { get; } internal bool FluentApiTypeIsInternal { get; } internal string DefaultAccessModifier { get; } - internal bool FluentApiTypeHasPrivateConstructor { get; } + internal ConstructorInfo FluentApiTypeConstructorInfo { get; } internal string BuilderClassName { get; } internal string BuilderClassNameWithTypeParameters { get; } internal string BuilderInstanceName { get; } diff --git a/src/M31.FluentApi.Generator/CodeGeneration/CodeGenerator.cs b/src/M31.FluentApi.Generator/CodeGeneration/CodeGenerator.cs index 5ee2190..51b7e22 100644 --- a/src/M31.FluentApi.Generator/CodeGeneration/CodeGenerator.cs +++ b/src/M31.FluentApi.Generator/CodeGeneration/CodeGenerator.cs @@ -19,7 +19,7 @@ internal static CodeGeneratorResult GenerateCode(FluentApiClassInfo classInfo, C classInfo.GenericInfo, classInfo.IsStruct, classInfo.IsInternal, - classInfo.HasPrivateConstructor, + classInfo.ConstructorInfo, classInfo.BuilderClassName); CodeBoard codeBoard = CodeBoard.Create( diff --git a/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj b/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj index fd5936a..35f958d 100644 --- a/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj +++ b/src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj @@ -11,7 +11,7 @@ true true true - 1.7.0 + 1.8.0 Kevin Schaal The generator package for M31.FluentAPI. Don't install this package explicitly, install M31.FluentAPI instead. fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration diff --git a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs index 21f528b..0847f9c 100644 --- a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs +++ b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs @@ -66,11 +66,6 @@ private void AnalyzeNodeInternal(SyntaxNodeAnalysisContext context) return; } - if (!symbol.InstanceConstructors.Any(m => m.Parameters.Length == 0)) - { - context.ReportDiagnostic(MissingDefaultConstructor.CreateDiagnostic(symbol)); - } - ClassInfoResult classInfoResult = ClassInfoFactory.CreateFluentApiClassInfo( context.SemanticModel, diff --git a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs index d86a093..4572acf 100644 --- a/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs +++ b/src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs @@ -21,7 +21,6 @@ internal static class FluentApiDiagnostics InvalidFluentPredicateType.Descriptor, InvalidFluentNullableType.Descriptor, FluentNullableTypeWithoutNullableAnnotation.Descriptor, - MissingDefaultConstructor.Descriptor, CodeGenerationException.Descriptor, GenericException.Descriptor, OrthogonalAttributeMisusedWithCompound.Descriptor, @@ -33,6 +32,7 @@ internal static class FluentApiDiagnostics ReservedMethodName.Descriptor, FluentLambdaMemberWithoutFluentApi.Descriptor, LastBuilderStepCannotBeSkipped.Descriptor, + AmbiguousConstructors.Descriptor, }; internal static class MissingSetAccessor @@ -195,23 +195,6 @@ internal static Diagnostic CreateDiagnostic(TypeSyntax actualType) } } - internal static class MissingDefaultConstructor - { - internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( - id: "M31FA011", - title: "Default constructor is missing", - messageFormat: "The fluent API requires a default constructor. " + - "Add a default constructor to type '{0}'.", - category: "M31.Usage", - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true); - - internal static Diagnostic CreateDiagnostic(INamedTypeSymbol symbol) - { - return Diagnostic.Create(Descriptor, symbol.Locations[0], symbol.Name); - } - } - /// /// Diagnostic used for s. /// @@ -409,4 +392,21 @@ internal static Diagnostic CreateDiagnostic(AttributeDataExtended attributeData) return Diagnostic.Create(Descriptor, location); } } + + internal static class AmbiguousConstructors + { + internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + id: "M31FA024", + title: "Constructors are ambiguous", + messageFormat: "The fluent API creates instances by invoking the constructor with the fewest parameters " + + "with default values. Found more than one constructor with {0} parameter(s).", + category: "M31.Usage", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static Diagnostic CreateDiagnostic(IMethodSymbol constructorSymbol, int numberOfParameters) + { + return Diagnostic.Create(Descriptor, constructorSymbol.Locations[0], numberOfParameters); + } + } } \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/SourceGenerators/ClassInfoFactory.cs b/src/M31.FluentApi.Generator/SourceGenerators/ClassInfoFactory.cs index 85386c1..7cd340e 100644 --- a/src/M31.FluentApi.Generator/SourceGenerators/ClassInfoFactory.cs +++ b/src/M31.FluentApi.Generator/SourceGenerators/ClassInfoFactory.cs @@ -1,3 +1,4 @@ +using M31.FluentApi.Generator.Commons; using M31.FluentApi.Generator.SourceGenerators.AttributeElements; using M31.FluentApi.Generator.SourceGenerators.AttributeInfo; using M31.FluentApi.Generator.SourceGenerators.Generics; @@ -85,7 +86,7 @@ private ClassInfoResult CreateFluentApiClassInfoInternal( string className = type.Name; string? @namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToString(); bool isInternal = type.DeclaredAccessibility == Accessibility.Internal; - bool hasPrivateConstructor = HasPrivateConstructor(type); + ConstructorInfo? constructorInfo = TryGetConstructorInfo(type); FluentApiAttributeInfo fluentApiAttributeInfo = FluentApiAttributeInfo.Create(attributeDataExtended.AttributeData, className); @@ -114,7 +115,7 @@ private ClassInfoResult CreateFluentApiClassInfoInternal( genericInfo, isStruct, isInternal, - hasPrivateConstructor, + constructorInfo!, fluentApiAttributeInfo.BuilderClassName, newLineString, infos, @@ -122,17 +123,46 @@ private ClassInfoResult CreateFluentApiClassInfoInternal( new FluentApiClassAdditionalInfo(groups)); } - private bool HasPrivateConstructor(INamedTypeSymbol type) + private ConstructorInfo? TryGetConstructorInfo(INamedTypeSymbol type) { - IMethodSymbol[] defaultInstanceConstructors = - type.InstanceConstructors.Where(c => c.Parameters.Length == 0).ToArray(); + /* Look for the default constructor. If it is not present, take the constructor + with the fewest parameters that is explicitly declared. */ + +#pragma warning disable RS1024 + IGrouping[] constructorsGroupedByNumberOfParameters = + type.InstanceConstructors + .Where(c => c.Parameters.Length == 0 || !c.IsImplicitlyDeclared) + .GroupBy(c => c.Parameters.Length) + .OrderBy(g => g.Key) + .ToArray(); +#pragma warning restore RS1024 + + IGrouping? constructorsWithFewestParameters = + constructorsGroupedByNumberOfParameters.FirstOrDefault(); + + if (constructorsWithFewestParameters == null) + { + throw new GenerationException( + $"The type {type.Name} has neither a default constructor nor explicitly declared constructors."); + } - if (defaultInstanceConstructors.Length == 0) + IMethodSymbol[] constructors = constructorsWithFewestParameters.ToArray(); + + if (constructors.Length != 1) { - return false; + int nofParameters = constructorsWithFewestParameters.Key; + + foreach (IMethodSymbol constructor in constructors) + { + report.ReportDiagnostic(AmbiguousConstructors.CreateDiagnostic(constructor, nofParameters)); + } + + return null; } - return !defaultInstanceConstructors.Any(c => c.DeclaredAccessibility == Accessibility.Public); + return new ConstructorInfo( + constructors[0].Parameters.Length, + constructors[0].DeclaredAccessibility != Accessibility.Public); } private FluentApiInfo? TryCreateFluentApiInfo(ISymbol symbol) diff --git a/src/M31.FluentApi.Generator/SourceGenerators/ConstructorInfo.cs b/src/M31.FluentApi.Generator/SourceGenerators/ConstructorInfo.cs new file mode 100644 index 0000000..b7ddedb --- /dev/null +++ b/src/M31.FluentApi.Generator/SourceGenerators/ConstructorInfo.cs @@ -0,0 +1,13 @@ +namespace M31.FluentApi.Generator.SourceGenerators; + +internal record ConstructorInfo +{ + public ConstructorInfo(int numberOfParameters, bool constructorIsNonPublic) + { + NumberOfParameters = numberOfParameters; + ConstructorIsNonPublic = constructorIsNonPublic; + } + + internal int NumberOfParameters { get; } + internal bool ConstructorIsNonPublic { get; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Generator/SourceGenerators/FluentApiClassInfo.cs b/src/M31.FluentApi.Generator/SourceGenerators/FluentApiClassInfo.cs index ddb12e7..f4bdf0b 100644 --- a/src/M31.FluentApi.Generator/SourceGenerators/FluentApiClassInfo.cs +++ b/src/M31.FluentApi.Generator/SourceGenerators/FluentApiClassInfo.cs @@ -16,7 +16,7 @@ internal FluentApiClassInfo( GenericInfo? genericInfo, bool isStruct, bool isInternal, - bool hasPrivateConstructor, + ConstructorInfo constructorInfo, string builderClassName, string newLineString, IReadOnlyCollection fluentApiInfos, @@ -28,7 +28,7 @@ internal FluentApiClassInfo( GenericInfo = genericInfo; IsStruct = isStruct; IsInternal = isInternal; - HasPrivateConstructor = hasPrivateConstructor; + ConstructorInfo = constructorInfo; BuilderClassName = builderClassName; NewLineString = newLineString; FluentApiInfos = fluentApiInfos; @@ -41,7 +41,7 @@ internal FluentApiClassInfo( internal GenericInfo? GenericInfo { get; } internal bool IsStruct { get; } internal bool IsInternal { get; } - internal bool HasPrivateConstructor { get; } + internal ConstructorInfo ConstructorInfo { get; } internal string BuilderClassName { get; } internal string NewLineString { get; } internal IReadOnlyCollection FluentApiInfos { get; } @@ -57,7 +57,7 @@ public bool Equals(FluentApiClassInfo? other) Equals(GenericInfo, other.GenericInfo) && IsStruct == other.IsStruct && IsInternal == other.IsInternal && - HasPrivateConstructor == other.HasPrivateConstructor && + ConstructorInfo.Equals(other.ConstructorInfo) && BuilderClassName == other.BuilderClassName && NewLineString == other.NewLineString && FluentApiInfos.SequenceEqual(other.FluentApiInfos) && @@ -76,7 +76,7 @@ public override int GetHashCode() { return new HashCode() .Add(Name, Namespace, GenericInfo) - .Add(IsStruct, IsInternal, HasPrivateConstructor) + .Add(IsStruct, IsInternal, ConstructorInfo) .Add(BuilderClassName) .Add(NewLineString) .AddSequence(FluentApiInfos) diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs index 9f341b9..95f5668 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs @@ -12,6 +12,22 @@ namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes; public class AnalyzerAndCodeFixTests { + [Fact] + public async Task CanDetectAmbiguousConstructors() + { + SourceWithFix source = ReadSource("AmbiguousConstructorsClass", "Student"); + + var expectedDiagnostic1 = Verifier.Diagnostic(AmbiguousConstructors.Descriptor.Id) + .WithLocation(10, 12) + .WithArguments(1); + + var expectedDiagnostic2 = Verifier.Diagnostic(AmbiguousConstructors.Descriptor.Id) + .WithLocation(15, 12) + .WithArguments(1); + + await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic1, expectedDiagnostic2); + } + [Fact] public async Task CanDetectConflictingControlAttributes1() { @@ -188,18 +204,6 @@ public async Task CanDetectMissingBuilderStep() await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); } - [Fact] - public async Task CanDetectMissingDefaultConstructor() - { - SourceWithFix source = ReadSource("MissingDefaultConstructorClass", "Student"); - - var expectedDiagnostic = Verifier.Diagnostic(MissingDefaultConstructor.Descriptor.Id) - .WithLocation(8, 14) - .WithArguments("Student"); - - await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic); - } - [Fact] public async Task CanDetectNullableTypeNoNullableAnnotation() { diff --git a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/MissingDefaultConstructorClass/Student.cs b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/AmbiguousConstructorsClass/Student.cs similarity index 56% rename from src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/MissingDefaultConstructorClass/Student.cs rename to src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/AmbiguousConstructorsClass/Student.cs index 54732f5..684c5ef 100644 --- a/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/MissingDefaultConstructorClass/Student.cs +++ b/src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/TestClasses/AmbiguousConstructorsClass/Student.cs @@ -2,7 +2,7 @@ using M31.FluentApi.Attributes; -namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.MissingDefaultConstructorClass; +namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes.TestClasses.AmbiguousConstructorsClass; [FluentApi] public class Student @@ -12,6 +12,11 @@ public Student(int semester) Semester = semester; } + public Student(string semester) + { + Semester = int.Parse(semester); + } + [FluentMember(0)] public int Semester { get; set; } } \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs b/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs index 158538b..04770f8 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/CodeGenerationExecutionTests.cs @@ -311,6 +311,17 @@ public void CanExecuteFluentReturnSingleStepPrivateMethodsClass() } } + [Fact, Priority(1)] + public void CanExecuteGenericClassPrivateConstructor() + { + var student = TestClasses.Abstract.GenericClassPrivateConstructor + .CreateStudent + .WithProperty1(10); + + Assert.Equal(10, student.Property1); + Assert.Null(student.Property2); + } + [Fact, Priority(1)] public void CanExecuteGenericClassWithGenericMethods() { @@ -610,6 +621,20 @@ public void CanExecuteThreeMemberClass() Assert.Equal(2, student.Semester); } + [Fact, Priority(1)] + public void CanExecuteThreeMemberRecordPrimaryConstructor() + { + var student = TestClasses.Abstract.ThreeMemberRecordPrimaryConstructor + .CreateStudent + .WithName("Alice") + .BornOn(new DateOnly(2002, 8, 3)) + .InSemester(2); + + Assert.Equal("Alice", student.name); + Assert.Equal(new DateOnly(2002, 8, 3), student.dateOfBirth); + Assert.Equal(2, student.semester); + } + [Fact, Priority(1)] public void CanExecuteThreePrivateMembersClass() { diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.expected.txt b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.expected.txt index f210fc7..c71f86a 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.expected.txt +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.expected.txt @@ -6,6 +6,7 @@ #nullable enable using M31.FluentApi.Attributes; +using System.Reflection; using System; namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.GenericClassPrivateConstructor; @@ -18,7 +19,7 @@ public class CreateStudent : private CreateStudent() { - student = (Student) Activator.CreateInstance(typeof(Student), true)!; + student = (Student) Activator.CreateInstance(typeof(Student), BindingFlags.Instance | BindingFlags.NonPublic, null, new object?[] { null, null }, null)!; } public static ICreateStudent InitialStep() diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.g.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.g.cs index f210fc7..c71f86a 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.g.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/CreateStudent.g.cs @@ -6,6 +6,7 @@ #nullable enable using M31.FluentApi.Attributes; +using System.Reflection; using System; namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.GenericClassPrivateConstructor; @@ -18,7 +19,7 @@ public class CreateStudent : private CreateStudent() { - student = (Student) Activator.CreateInstance(typeof(Student), true)!; + student = (Student) Activator.CreateInstance(typeof(Student), BindingFlags.Instance | BindingFlags.NonPublic, null, new object?[] { null, null }, null)!; } public static ICreateStudent InitialStep() diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/Student.cs index b25e5d9..c4b49b9 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/Student.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateConstructor/Student.cs @@ -9,9 +9,10 @@ namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.GenericClassPr [FluentApi] public class Student { - private Student() + private Student(T1 property1, T2 property2) { - + Property1 = property1; + Property2 = property2; } [FluentMember(0)] diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/CreateStudent.expected.txt b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/CreateStudent.expected.txt new file mode 100644 index 0000000..9502d63 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/CreateStudent.expected.txt @@ -0,0 +1,65 @@ +// +// This code was generated by the library M31.FluentAPI. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#nullable enable + +using M31.FluentApi.Attributes; +using System; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.GenericClassPrivateDefaultConstructor; + +public class CreateStudent : + CreateStudent.ICreateStudent, + CreateStudent.IWithProperty1WithProperty2 +{ + private readonly Student student; + + private CreateStudent() + { + student = (Student) Activator.CreateInstance(typeof(Student), true)!; + } + + public static ICreateStudent InitialStep() + { + return new CreateStudent(); + } + + public static Student WithProperty1(T1 property1) + { + CreateStudent createStudent = new CreateStudent(); + createStudent.student.Property1 = property1; + return createStudent.student; + } + + public static Student WithProperty2(T2 property2) + { + CreateStudent createStudent = new CreateStudent(); + createStudent.student.Property2 = property2; + return createStudent.student; + } + + Student IWithProperty1WithProperty2.WithProperty1(T1 property1) + { + student.Property1 = property1; + return student; + } + + Student IWithProperty1WithProperty2.WithProperty2(T2 property2) + { + student.Property2 = property2; + return student; + } + + public interface ICreateStudent : IWithProperty1WithProperty2 + { + } + + public interface IWithProperty1WithProperty2 + { + Student WithProperty1(T1 property1); + + Student WithProperty2(T2 property2); + } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/CreateStudent.g.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/CreateStudent.g.cs new file mode 100644 index 0000000..9502d63 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/CreateStudent.g.cs @@ -0,0 +1,65 @@ +// +// This code was generated by the library M31.FluentAPI. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#nullable enable + +using M31.FluentApi.Attributes; +using System; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.GenericClassPrivateDefaultConstructor; + +public class CreateStudent : + CreateStudent.ICreateStudent, + CreateStudent.IWithProperty1WithProperty2 +{ + private readonly Student student; + + private CreateStudent() + { + student = (Student) Activator.CreateInstance(typeof(Student), true)!; + } + + public static ICreateStudent InitialStep() + { + return new CreateStudent(); + } + + public static Student WithProperty1(T1 property1) + { + CreateStudent createStudent = new CreateStudent(); + createStudent.student.Property1 = property1; + return createStudent.student; + } + + public static Student WithProperty2(T2 property2) + { + CreateStudent createStudent = new CreateStudent(); + createStudent.student.Property2 = property2; + return createStudent.student; + } + + Student IWithProperty1WithProperty2.WithProperty1(T1 property1) + { + student.Property1 = property1; + return student; + } + + Student IWithProperty1WithProperty2.WithProperty2(T2 property2) + { + student.Property2 = property2; + return student; + } + + public interface ICreateStudent : IWithProperty1WithProperty2 + { + } + + public interface IWithProperty1WithProperty2 + { + Student WithProperty1(T1 property1); + + Student WithProperty2(T2 property2); + } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/Student.cs new file mode 100644 index 0000000..210ff24 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/GenericClassPrivateDefaultConstructor/Student.cs @@ -0,0 +1,22 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.GenericClassPrivateDefaultConstructor; + +[FluentApi] +public class Student +{ + private Student() + { + + } + + [FluentMember(0)] + public T1 Property1 { get; set; } + + [FluentMember(0)] + public T2 Property2 { get; set; } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/CreateStudent.expected.txt b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/CreateStudent.expected.txt new file mode 100644 index 0000000..22aa772 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/CreateStudent.expected.txt @@ -0,0 +1,86 @@ +// +// This code was generated by the library M31.FluentAPI. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#nullable enable + +using System; +using System.Reflection.Metadata; +using M31.FluentApi.Attributes; +using System.Reflection; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.ThreeMemberRecordPrimaryConstructor; + +public class CreateStudent : + CreateStudent.ICreateStudent, + CreateStudent.IWithName, + CreateStudent.IBornOn, + CreateStudent.IInSemester +{ + private readonly Student student; + private static readonly PropertyInfo namePropertyInfo; + private static readonly PropertyInfo dateOfBirthPropertyInfo; + private static readonly PropertyInfo semesterPropertyInfo; + + static CreateStudent() + { + namePropertyInfo = typeof(Student).GetProperty("name", BindingFlags.Instance | BindingFlags.Public)!; + dateOfBirthPropertyInfo = typeof(Student).GetProperty("dateOfBirth", BindingFlags.Instance | BindingFlags.Public)!; + semesterPropertyInfo = typeof(Student).GetProperty("semester", BindingFlags.Instance | BindingFlags.Public)!; + } + + private CreateStudent() + { + student = new Student(default!, default!, default!); + } + + public static ICreateStudent InitialStep() + { + return new CreateStudent(); + } + + public static IBornOn WithName(string name) + { + CreateStudent createStudent = new CreateStudent(); + CreateStudent.namePropertyInfo.SetValue(createStudent.student, name); + return createStudent; + } + + IBornOn IWithName.WithName(string name) + { + CreateStudent.namePropertyInfo.SetValue(student, name); + return this; + } + + IInSemester IBornOn.BornOn(System.DateOnly dateOfBirth) + { + CreateStudent.dateOfBirthPropertyInfo.SetValue(student, dateOfBirth); + return this; + } + + Student IInSemester.InSemester(int semester) + { + CreateStudent.semesterPropertyInfo.SetValue(student, semester); + return student; + } + + public interface ICreateStudent : IWithName + { + } + + public interface IWithName + { + IBornOn WithName(string name); + } + + public interface IBornOn + { + IInSemester BornOn(System.DateOnly dateOfBirth); + } + + public interface IInSemester + { + Student InSemester(int semester); + } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/CreateStudent.g.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/CreateStudent.g.cs new file mode 100644 index 0000000..22aa772 --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/CreateStudent.g.cs @@ -0,0 +1,86 @@ +// +// This code was generated by the library M31.FluentAPI. +// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +#nullable enable + +using System; +using System.Reflection.Metadata; +using M31.FluentApi.Attributes; +using System.Reflection; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.ThreeMemberRecordPrimaryConstructor; + +public class CreateStudent : + CreateStudent.ICreateStudent, + CreateStudent.IWithName, + CreateStudent.IBornOn, + CreateStudent.IInSemester +{ + private readonly Student student; + private static readonly PropertyInfo namePropertyInfo; + private static readonly PropertyInfo dateOfBirthPropertyInfo; + private static readonly PropertyInfo semesterPropertyInfo; + + static CreateStudent() + { + namePropertyInfo = typeof(Student).GetProperty("name", BindingFlags.Instance | BindingFlags.Public)!; + dateOfBirthPropertyInfo = typeof(Student).GetProperty("dateOfBirth", BindingFlags.Instance | BindingFlags.Public)!; + semesterPropertyInfo = typeof(Student).GetProperty("semester", BindingFlags.Instance | BindingFlags.Public)!; + } + + private CreateStudent() + { + student = new Student(default!, default!, default!); + } + + public static ICreateStudent InitialStep() + { + return new CreateStudent(); + } + + public static IBornOn WithName(string name) + { + CreateStudent createStudent = new CreateStudent(); + CreateStudent.namePropertyInfo.SetValue(createStudent.student, name); + return createStudent; + } + + IBornOn IWithName.WithName(string name) + { + CreateStudent.namePropertyInfo.SetValue(student, name); + return this; + } + + IInSemester IBornOn.BornOn(System.DateOnly dateOfBirth) + { + CreateStudent.dateOfBirthPropertyInfo.SetValue(student, dateOfBirth); + return this; + } + + Student IInSemester.InSemester(int semester) + { + CreateStudent.semesterPropertyInfo.SetValue(student, semester); + return student; + } + + public interface ICreateStudent : IWithName + { + } + + public interface IWithName + { + IBornOn WithName(string name); + } + + public interface IBornOn + { + IInSemester BornOn(System.DateOnly dateOfBirth); + } + + public interface IInSemester + { + Student InSemester(int semester); + } +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/Student.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/Student.cs new file mode 100644 index 0000000..c3f25db --- /dev/null +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestClasses/Abstract/ThreeMemberRecordPrimaryConstructor/Student.cs @@ -0,0 +1,17 @@ +// Non-nullable member is uninitialized +#pragma warning disable CS8618 +// ReSharper disable All + +using System; +using System.Reflection.Metadata; +using M31.FluentApi.Attributes; + +namespace M31.FluentApi.Tests.CodeGeneration.TestClasses.Abstract.ThreeMemberRecordPrimaryConstructor; + +[FluentApi] +public record Student( + [property: FluentMember(0, "WithName")] string name, + [property: FluentMember(1, "BornOn")] DateOnly dateOfBirth, + [property: FluentMember(2, "InSemester")] int semester) +{ +} \ No newline at end of file diff --git a/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs b/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs index 7ea7887..cba4ff3 100644 --- a/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs +++ b/src/M31.FluentApi.Tests/CodeGeneration/TestDataProvider.cs @@ -46,6 +46,7 @@ internal class TestDataProvider : IEnumerable new object[] { "Abstract", "FullyQualifiedTypeClass", "Student" }, new object[] { "Abstract", "GenericClass", "Student" }, new object[] { "Abstract", "GenericClassPrivateConstructor", "Student" }, + new object[] { "Abstract", "GenericClassPrivateDefaultConstructor", "Student" }, new object[] { "Abstract", "GenericClassWithConstraints", "Student" }, new object[] { "Abstract", "GenericClassWithGenericMethods", "Student" }, new object[] { "Abstract", "GenericClassWithPrivateGenericMethods", "Student" }, @@ -83,6 +84,7 @@ internal class TestDataProvider : IEnumerable new object[] { "Abstract", "SkippableTwoLoopsClass", "Student" }, new object[] { "Abstract", "ThreeMemberClass", "Student" }, new object[] { "Abstract", "ThreeMemberRecord", "Student" }, + new object[] { "Abstract", "ThreeMemberRecordPrimaryConstructor", "Student" }, new object[] { "Abstract", "ThreeMemberStruct", "Student" }, new object[] { "Abstract", "ThreePrivateMembersClass", "Student" }, new object[] { "Abstract", "ThreeMemberRecordStruct", "Student" }, diff --git a/src/M31.FluentApi/M31.FluentApi.csproj b/src/M31.FluentApi/M31.FluentApi.csproj index a83af37..84ee6e1 100644 --- a/src/M31.FluentApi/M31.FluentApi.csproj +++ b/src/M31.FluentApi/M31.FluentApi.csproj @@ -7,7 +7,7 @@ enable true true - 1.7.0 + 1.8.0 Kevin Schaal Generate fluent builders in C#. fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration