Skip to content

Commit 85188ca

Browse files
authored
feat: Constructor selection (#30)
* feat: use constructor with least amount of parameters * refactor: rename test * test: GenericClassPrivateConstructor * fix(ConstructorGenerator): add BindingFlags.NonPublic * feat: report diagnostics for missing or ambiguous constructor * chore: adjust analyzer release tracking * fix(Analyzers): remove MissingConstructor diagnosis * test: CanDetectAmbiguousConstructors * chore: increase nuget versions to 1.8 * chore(ConstructorGenerator): add doc comment * fix(ClassInfoFactory): comment * refactor(ClassInfoFactory): improve variable name
1 parent 790e19c commit 85188ca

File tree

25 files changed

+524
-69
lines changed

25 files changed

+524
-69
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PM> Install-Package M31.FluentApi
3737
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:
3838

3939
```xml
40-
<PackageReference Include="M31.FluentApi" Version="1.7.0" PrivateAssets="all"/>
40+
<PackageReference Include="M31.FluentApi" Version="1.8.0" PrivateAssets="all"/>
4141
```
4242

4343
If you would like to examine the generated code, you may emit it by adding the following lines to your `csproj` file:

src/M31.FluentApi.Generator/AnalyzerReleases.Shipped.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,19 @@ M31FA007 | M31.Usage | Error | Partial types are not supported
6868

6969
Rule ID | Category | Severity | Notes
7070
--------|----------|----------|-------
71-
M31FA023 | M31.Usage | Error | Last builder step cannot be skipped
71+
M31FA023 | M31.Usage | Error | Last builder step cannot be skipped
72+
73+
74+
## Release 1.8.0
75+
76+
### Removed Rules
77+
78+
Rule ID | Category | Severity | Notes
79+
--------|----------|----------|-------
80+
M31FA011 | M31.Usage | Error | Default constructor is missing
81+
82+
### New Rules
83+
84+
Rule ID | Category | Severity | Notes
85+
--------|----------|----------|-------
86+
M31FA024 | M31.Usage | Error | Constructors are ambiguous

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/ConstructorGenerator.cs

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,41 @@ public void Modify(CodeBoard codeBoard)
1818
}
1919

2020
Method constructor = CreateConstructor(codeBoard.Info.BuilderClassName);
21+
int nofParameters = codeBoard.Info.FluentApiTypeConstructorInfo.NumberOfParameters;
2122

22-
if (codeBoard.Info.FluentApiTypeHasPrivateConstructor)
23+
if (codeBoard.Info.FluentApiTypeConstructorInfo.ConstructorIsNonPublic)
2324
{
24-
// student = (Student<T1, T2>) Activator.CreateInstance(typeof(Student<T1, T2>), true)!;
25-
constructor.AppendBodyLine(
26-
$"{instanceName} = ({classNameWithTypeParameters}) " +
27-
$"Activator.CreateInstance(typeof({classNameWithTypeParameters}), true)!;");
25+
if (nofParameters == 0)
26+
{
27+
// student = (Student<T1, T2>) Activator.CreateInstance(typeof(Student<T1, T2>), true)!;
28+
constructor.AppendBodyLine(
29+
$"{instanceName} = ({classNameWithTypeParameters}) " +
30+
$"Activator.CreateInstance(typeof({classNameWithTypeParameters}), true)!;");
2831

29-
codeBoard.CodeFile.AddUsing("System");
32+
codeBoard.CodeFile.AddUsing("System");
33+
}
34+
else
35+
{
36+
// student = (Student<T1, T2>) Activator.CreateInstance(typeof(Student<T1, T2>), BindingFlags.Instance |
37+
// BindingFlags.NonPublic, null, new object?[] { null, null }, null)!;
38+
string parameters =
39+
$"new object?[] {{ {string.Join(", ", Enumerable.Repeat("null", nofParameters))} }}";
40+
41+
constructor.AppendBodyLine(
42+
$"{instanceName} = ({classNameWithTypeParameters}) " +
43+
$"Activator.CreateInstance(" +
44+
$"typeof({classNameWithTypeParameters}), BindingFlags.Instance | BindingFlags.NonPublic, null, {parameters}, null)!;");
45+
46+
codeBoard.CodeFile.AddUsing("System.Reflection");
47+
codeBoard.CodeFile.AddUsing("System");
48+
}
3049
}
3150
else
3251
{
33-
// student = new Student<T1, T2>();
34-
constructor.AppendBodyLine($"{instanceName} = new {classNameWithTypeParameters}();");
52+
// student = new Student<T1, T2>(default!, default!);
53+
string parameters = string.Join(", ",
54+
Enumerable.Repeat("default!", nofParameters));
55+
constructor.AppendBodyLine($"{instanceName} = new {classNameWithTypeParameters}({parameters});");
3556
}
3657

3758
codeBoard.Constructor = constructor;

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardElements/BuilderAndTargetInfo.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using M31.FluentApi.Generator.Commons;
2+
using M31.FluentApi.Generator.SourceGenerators;
23
using M31.FluentApi.Generator.SourceGenerators.Generics;
34

45
namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardElements;
@@ -11,7 +12,7 @@ internal BuilderAndTargetInfo(
1112
GenericInfo? genericInfo,
1213
bool fluentApiTypeIsStruct,
1314
bool fluentApiTypeIsInternal,
14-
bool fluentApiTypeHasPrivateConstructor,
15+
ConstructorInfo fluentApiTypeConstructorInfo,
1516
string builderClassName)
1617
{
1718
Namespace = @namespace;
@@ -21,7 +22,7 @@ internal BuilderAndTargetInfo(
2122
FluentApiTypeIsStruct = fluentApiTypeIsStruct;
2223
FluentApiTypeIsInternal = fluentApiTypeIsInternal;
2324
DefaultAccessModifier = fluentApiTypeIsInternal ? "internal" : "public";
24-
FluentApiTypeHasPrivateConstructor = fluentApiTypeHasPrivateConstructor;
25+
FluentApiTypeConstructorInfo = fluentApiTypeConstructorInfo;
2526
BuilderClassName = builderClassName;
2627
BuilderClassNameWithTypeParameters = WithTypeParameters(builderClassName, genericInfo);
2728
BuilderInstanceName = builderClassName.FirstCharToLower();
@@ -36,7 +37,7 @@ internal BuilderAndTargetInfo(
3637
internal bool FluentApiTypeIsStruct { get; }
3738
internal bool FluentApiTypeIsInternal { get; }
3839
internal string DefaultAccessModifier { get; }
39-
internal bool FluentApiTypeHasPrivateConstructor { get; }
40+
internal ConstructorInfo FluentApiTypeConstructorInfo { get; }
4041
internal string BuilderClassName { get; }
4142
internal string BuilderClassNameWithTypeParameters { get; }
4243
internal string BuilderInstanceName { get; }

src/M31.FluentApi.Generator/CodeGeneration/CodeGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal static CodeGeneratorResult GenerateCode(FluentApiClassInfo classInfo, C
1919
classInfo.GenericInfo,
2020
classInfo.IsStruct,
2121
classInfo.IsInternal,
22-
classInfo.HasPrivateConstructor,
22+
classInfo.ConstructorInfo,
2323
classInfo.BuilderClassName);
2424

2525
CodeBoard codeBoard = CodeBoard.Create(

src/M31.FluentApi.Generator/M31.FluentApi.Generator.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<GenerateDocumentationFile>true</GenerateDocumentationFile>
1212
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1313
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
14-
<PackageVersion>1.7.0</PackageVersion>
14+
<PackageVersion>1.8.0</PackageVersion>
1515
<Authors>Kevin Schaal</Authors>
1616
<Description>The generator package for M31.FluentAPI. Don't install this package explicitly, install M31.FluentAPI instead.</Description>
1717
<PackageTags>fluentapi fluentbuilder fluentinterface fluentdesign fluent codegeneration</PackageTags>

src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiAnalyzer.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,6 @@ private void AnalyzeNodeInternal(SyntaxNodeAnalysisContext context)
6666
return;
6767
}
6868

69-
if (!symbol.InstanceConstructors.Any(m => m.Parameters.Length == 0))
70-
{
71-
context.ReportDiagnostic(MissingDefaultConstructor.CreateDiagnostic(symbol));
72-
}
73-
7469
ClassInfoResult classInfoResult =
7570
ClassInfoFactory.CreateFluentApiClassInfo(
7671
context.SemanticModel,

src/M31.FluentApi.Generator/SourceAnalyzers/FluentApiDiagnostics.cs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ internal static class FluentApiDiagnostics
2121
InvalidFluentPredicateType.Descriptor,
2222
InvalidFluentNullableType.Descriptor,
2323
FluentNullableTypeWithoutNullableAnnotation.Descriptor,
24-
MissingDefaultConstructor.Descriptor,
2524
CodeGenerationException.Descriptor,
2625
GenericException.Descriptor,
2726
OrthogonalAttributeMisusedWithCompound.Descriptor,
@@ -33,6 +32,7 @@ internal static class FluentApiDiagnostics
3332
ReservedMethodName.Descriptor,
3433
FluentLambdaMemberWithoutFluentApi.Descriptor,
3534
LastBuilderStepCannotBeSkipped.Descriptor,
35+
AmbiguousConstructors.Descriptor,
3636
};
3737

3838
internal static class MissingSetAccessor
@@ -195,23 +195,6 @@ internal static Diagnostic CreateDiagnostic(TypeSyntax actualType)
195195
}
196196
}
197197

198-
internal static class MissingDefaultConstructor
199-
{
200-
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
201-
id: "M31FA011",
202-
title: "Default constructor is missing",
203-
messageFormat: "The fluent API requires a default constructor. " +
204-
"Add a default constructor to type '{0}'.",
205-
category: "M31.Usage",
206-
defaultSeverity: DiagnosticSeverity.Error,
207-
isEnabledByDefault: true);
208-
209-
internal static Diagnostic CreateDiagnostic(INamedTypeSymbol symbol)
210-
{
211-
return Diagnostic.Create(Descriptor, symbol.Locations[0], symbol.Name);
212-
}
213-
}
214-
215198
/// <summary>
216199
/// Diagnostic used for <see cref="GenerationException"/>s.
217200
/// </summary>
@@ -409,4 +392,21 @@ internal static Diagnostic CreateDiagnostic(AttributeDataExtended attributeData)
409392
return Diagnostic.Create(Descriptor, location);
410393
}
411394
}
395+
396+
internal static class AmbiguousConstructors
397+
{
398+
internal static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor(
399+
id: "M31FA024",
400+
title: "Constructors are ambiguous",
401+
messageFormat: "The fluent API creates instances by invoking the constructor with the fewest parameters " +
402+
"with default values. Found more than one constructor with {0} parameter(s).",
403+
category: "M31.Usage",
404+
defaultSeverity: DiagnosticSeverity.Error,
405+
isEnabledByDefault: true);
406+
407+
internal static Diagnostic CreateDiagnostic(IMethodSymbol constructorSymbol, int numberOfParameters)
408+
{
409+
return Diagnostic.Create(Descriptor, constructorSymbol.Locations[0], numberOfParameters);
410+
}
411+
}
412412
}

src/M31.FluentApi.Generator/SourceGenerators/ClassInfoFactory.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using M31.FluentApi.Generator.Commons;
12
using M31.FluentApi.Generator.SourceGenerators.AttributeElements;
23
using M31.FluentApi.Generator.SourceGenerators.AttributeInfo;
34
using M31.FluentApi.Generator.SourceGenerators.Generics;
@@ -85,7 +86,7 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
8586
string className = type.Name;
8687
string? @namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToString();
8788
bool isInternal = type.DeclaredAccessibility == Accessibility.Internal;
88-
bool hasPrivateConstructor = HasPrivateConstructor(type);
89+
ConstructorInfo? constructorInfo = TryGetConstructorInfo(type);
8990
FluentApiAttributeInfo fluentApiAttributeInfo =
9091
FluentApiAttributeInfo.Create(attributeDataExtended.AttributeData, className);
9192

@@ -114,25 +115,54 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
114115
genericInfo,
115116
isStruct,
116117
isInternal,
117-
hasPrivateConstructor,
118+
constructorInfo!,
118119
fluentApiAttributeInfo.BuilderClassName,
119120
newLineString,
120121
infos,
121122
usingStatements,
122123
new FluentApiClassAdditionalInfo(groups));
123124
}
124125

125-
private bool HasPrivateConstructor(INamedTypeSymbol type)
126+
private ConstructorInfo? TryGetConstructorInfo(INamedTypeSymbol type)
126127
{
127-
IMethodSymbol[] defaultInstanceConstructors =
128-
type.InstanceConstructors.Where(c => c.Parameters.Length == 0).ToArray();
128+
/* Look for the default constructor. If it is not present, take the constructor
129+
with the fewest parameters that is explicitly declared. */
130+
131+
#pragma warning disable RS1024
132+
IGrouping<int, IMethodSymbol>[] constructorsGroupedByNumberOfParameters =
133+
type.InstanceConstructors
134+
.Where(c => c.Parameters.Length == 0 || !c.IsImplicitlyDeclared)
135+
.GroupBy(c => c.Parameters.Length)
136+
.OrderBy(g => g.Key)
137+
.ToArray();
138+
#pragma warning restore RS1024
139+
140+
IGrouping<int, IMethodSymbol>? constructorsWithFewestParameters =
141+
constructorsGroupedByNumberOfParameters.FirstOrDefault();
142+
143+
if (constructorsWithFewestParameters == null)
144+
{
145+
throw new GenerationException(
146+
$"The type {type.Name} has neither a default constructor nor explicitly declared constructors.");
147+
}
129148

130-
if (defaultInstanceConstructors.Length == 0)
149+
IMethodSymbol[] constructors = constructorsWithFewestParameters.ToArray();
150+
151+
if (constructors.Length != 1)
131152
{
132-
return false;
153+
int nofParameters = constructorsWithFewestParameters.Key;
154+
155+
foreach (IMethodSymbol constructor in constructors)
156+
{
157+
report.ReportDiagnostic(AmbiguousConstructors.CreateDiagnostic(constructor, nofParameters));
158+
}
159+
160+
return null;
133161
}
134162

135-
return !defaultInstanceConstructors.Any(c => c.DeclaredAccessibility == Accessibility.Public);
163+
return new ConstructorInfo(
164+
constructors[0].Parameters.Length,
165+
constructors[0].DeclaredAccessibility != Accessibility.Public);
136166
}
137167

138168
private FluentApiInfo? TryCreateFluentApiInfo(ISymbol symbol)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace M31.FluentApi.Generator.SourceGenerators;
2+
3+
internal record ConstructorInfo
4+
{
5+
public ConstructorInfo(int numberOfParameters, bool constructorIsNonPublic)
6+
{
7+
NumberOfParameters = numberOfParameters;
8+
ConstructorIsNonPublic = constructorIsNonPublic;
9+
}
10+
11+
internal int NumberOfParameters { get; }
12+
internal bool ConstructorIsNonPublic { get; }
13+
}

src/M31.FluentApi.Generator/SourceGenerators/FluentApiClassInfo.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal FluentApiClassInfo(
1616
GenericInfo? genericInfo,
1717
bool isStruct,
1818
bool isInternal,
19-
bool hasPrivateConstructor,
19+
ConstructorInfo constructorInfo,
2020
string builderClassName,
2121
string newLineString,
2222
IReadOnlyCollection<FluentApiInfo> fluentApiInfos,
@@ -28,7 +28,7 @@ internal FluentApiClassInfo(
2828
GenericInfo = genericInfo;
2929
IsStruct = isStruct;
3030
IsInternal = isInternal;
31-
HasPrivateConstructor = hasPrivateConstructor;
31+
ConstructorInfo = constructorInfo;
3232
BuilderClassName = builderClassName;
3333
NewLineString = newLineString;
3434
FluentApiInfos = fluentApiInfos;
@@ -41,7 +41,7 @@ internal FluentApiClassInfo(
4141
internal GenericInfo? GenericInfo { get; }
4242
internal bool IsStruct { get; }
4343
internal bool IsInternal { get; }
44-
internal bool HasPrivateConstructor { get; }
44+
internal ConstructorInfo ConstructorInfo { get; }
4545
internal string BuilderClassName { get; }
4646
internal string NewLineString { get; }
4747
internal IReadOnlyCollection<FluentApiInfo> FluentApiInfos { get; }
@@ -57,7 +57,7 @@ public bool Equals(FluentApiClassInfo? other)
5757
Equals(GenericInfo, other.GenericInfo) &&
5858
IsStruct == other.IsStruct &&
5959
IsInternal == other.IsInternal &&
60-
HasPrivateConstructor == other.HasPrivateConstructor &&
60+
ConstructorInfo.Equals(other.ConstructorInfo) &&
6161
BuilderClassName == other.BuilderClassName &&
6262
NewLineString == other.NewLineString &&
6363
FluentApiInfos.SequenceEqual(other.FluentApiInfos) &&
@@ -76,7 +76,7 @@ public override int GetHashCode()
7676
{
7777
return new HashCode()
7878
.Add(Name, Namespace, GenericInfo)
79-
.Add(IsStruct, IsInternal, HasPrivateConstructor)
79+
.Add(IsStruct, IsInternal, ConstructorInfo)
8080
.Add(BuilderClassName)
8181
.Add(NewLineString)
8282
.AddSequence(FluentApiInfos)

src/M31.FluentApi.Tests/AnalyzerAndCodeFixes/AnalyzerAndCodeFixTests.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ namespace M31.FluentApi.Tests.AnalyzerAndCodeFixes;
1212

1313
public class AnalyzerAndCodeFixTests
1414
{
15+
[Fact]
16+
public async Task CanDetectAmbiguousConstructors()
17+
{
18+
SourceWithFix source = ReadSource("AmbiguousConstructorsClass", "Student");
19+
20+
var expectedDiagnostic1 = Verifier.Diagnostic(AmbiguousConstructors.Descriptor.Id)
21+
.WithLocation(10, 12)
22+
.WithArguments(1);
23+
24+
var expectedDiagnostic2 = Verifier.Diagnostic(AmbiguousConstructors.Descriptor.Id)
25+
.WithLocation(15, 12)
26+
.WithArguments(1);
27+
28+
await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic1, expectedDiagnostic2);
29+
}
30+
1531
[Fact]
1632
public async Task CanDetectConflictingControlAttributes1()
1733
{
@@ -188,18 +204,6 @@ public async Task CanDetectMissingBuilderStep()
188204
await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic);
189205
}
190206

191-
[Fact]
192-
public async Task CanDetectMissingDefaultConstructor()
193-
{
194-
SourceWithFix source = ReadSource("MissingDefaultConstructorClass", "Student");
195-
196-
var expectedDiagnostic = Verifier.Diagnostic(MissingDefaultConstructor.Descriptor.Id)
197-
.WithLocation(8, 14)
198-
.WithArguments("Student");
199-
200-
await Verifier.VerifyCodeFixAsync(source, expectedDiagnostic);
201-
}
202-
203207
[Fact]
204208
public async Task CanDetectNullableTypeNoNullableAnnotation()
205209
{

0 commit comments

Comments
 (0)