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