Skip to content

Commit 5811737

Browse files
oformaniukOleh Formaniukm31coding
authored
feat: Inheritance (#32)
* Inheritance implementation for Fluent API * refactor(ClassInfoFactory): minor changes * test: InheritedClass * fix(ClassInfoFactory): compare to SpecialType in GetMembers * test: InheritedClassProtectedMembers and InheritedClassProtectedSetters * docs: mention inheritance in features * fix: inheritance tests * test: inherited record * improve(ExampleProject): add ExchangeStudent example * test: add failing test CanExecuteInheritedClassPrivateSetters * fix: make test InheritedClassPrivateSetters work * fix: InnerBodyForMethodGenerator * refactor(ClassInfoFactory): use List instead of HashSet * fix: commit generated file * chore: bump nuget version --------- Co-authored-by: Oleh Formaniuk <[email protected]> Co-authored-by: Kevin Schaal <[email protected]>
1 parent 85188ca commit 5811737

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1620
-59
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Accompanying blog post: [www.m31coding.com>blog>fluent-api](https://www.m31codin
2424
- Optional (skippable) builder methods
2525
- Forking and branching capabilities
2626
- Support for returning arbitrary types
27-
- Support for generics and partial classes
27+
- Support for inheritance, generics, and partial classes
2828

2929
## Installing via NuGet
3030

@@ -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.8.0" PrivateAssets="all"/>
40+
<PackageReference Include="M31.FluentApi" Version="1.9.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/ExampleProject/ExchangeStudent.cs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Non-nullable member is uninitialized
2+
#pragma warning disable CS8618
3+
// ReSharper disable All
4+
5+
using M31.FluentApi.Attributes;
6+
7+
namespace ExampleProject;
8+
9+
[FluentApi]
10+
public class ExchangeStudent : Student
11+
{
12+
[FluentMember(6)]
13+
public string HomeCountry { get; private set; }
14+
}

src/ExampleProject/Program.cs

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
Console.WriteLine(JsonSerializer.Serialize(student1));
1313
Console.WriteLine(JsonSerializer.Serialize(student2));
1414

15+
// ExchangeStudent (inherited from Student)
16+
//
17+
18+
ExchangeStudent exchangeStudent = CreateExchangeStudent.Named("Bob", "Bishop").BornOn(new DateOnly(2002, 8, 3))
19+
.InSemester(2).LivingInBoston().WithUnknownMood().WhoseFriendIs("Alice").WithHomeCountry("United States");
20+
21+
Console.WriteLine(JsonSerializer.Serialize(exchangeStudent));
22+
1523
// Person
1624
//
1725

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/InnerBodyGeneration/InnerBodyForMemberGenerator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected override void InitializeInfoField(string fieldName, MemberSymbolInfo s
4343
// semesterPropertyInfo = typeof(Student<T1, T2>)
4444
// .GetProperty("Semester", BindingFlags.Instance | BindingFlags.NonPublic););
4545
string code = $"{fieldName} =" +
46-
$" typeof({CodeBoard.Info.FluentApiClassNameWithTypeParameters})" +
46+
$" typeof({symbolInfo.DeclaringClassNameWithTypeParameters})" +
4747
$".Get{SymbolType(symbolInfo)}(\"{symbolInfo.Name}\", " +
4848
$"{InfoFieldBindingFlagsArgument(symbolInfo)})!;";
4949

src/M31.FluentApi.Generator/CodeGeneration/CodeBoardActors/InnerBodyGeneration/InnerBodyForMethodGenerator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ protected override void InitializeInfoField(string fieldName, MethodSymbolInfo s
194194
// Generic types are created via Type.MakeGenericMethodParameter(int position). In addition, a ref type is
195195
// specified via MakeByRefType().
196196
staticConstructor.AppendBodyLine($"{fieldName} = " +
197-
$"typeof({CodeBoard.Info.FluentApiClassNameWithTypeParameters}).GetMethod(");
197+
$"typeof({symbolInfo.DeclaringClassNameWithTypeParameters}).GetMethod(");
198198
staticConstructor.AppendBodyLine($"{indentation}\"{symbolInfo.Name}\",");
199199
staticConstructor.AppendBodyLine($"{indentation}{GetGenericParameterCount(symbolInfo.GenericInfo)},");
200200
staticConstructor.AppendBodyLine($"{indentation}{InfoFieldBindingFlagsArgument(symbolInfo)},");

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

+4-8
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ internal BuilderAndTargetInfo(
1717
{
1818
Namespace = @namespace;
1919
FluentApiClassName = fluentApiClassName;
20-
FluentApiClassNameWithTypeParameters = WithTypeParameters(fluentApiClassName, genericInfo);
20+
FluentApiClassNameWithTypeParameters =
21+
ClassInfoFactory.AugmentTypeNameWithGenericParameters(fluentApiClassName, genericInfo);
2122
GenericInfo = genericInfo;
2223
FluentApiTypeIsStruct = fluentApiTypeIsStruct;
2324
FluentApiTypeIsInternal = fluentApiTypeIsInternal;
2425
DefaultAccessModifier = fluentApiTypeIsInternal ? "internal" : "public";
2526
FluentApiTypeConstructorInfo = fluentApiTypeConstructorInfo;
2627
BuilderClassName = builderClassName;
27-
BuilderClassNameWithTypeParameters = WithTypeParameters(builderClassName, genericInfo);
28+
BuilderClassNameWithTypeParameters =
29+
ClassInfoFactory.AugmentTypeNameWithGenericParameters(builderClassName, genericInfo);
2830
BuilderInstanceName = builderClassName.FirstCharToLower();
2931
ClassInstanceName = fluentApiClassName.FirstCharToLower();
3032
InitialStepInterfaceName = $"I{builderClassName}";
@@ -43,10 +45,4 @@ internal BuilderAndTargetInfo(
4345
internal string BuilderInstanceName { get; }
4446
internal string ClassInstanceName { get; }
4547
internal string InitialStepInterfaceName { get; }
46-
47-
private static string WithTypeParameters(string typeName, GenericInfo? genericInfo)
48-
{
49-
string parameterListInAngleBrackets = genericInfo?.ParameterListInAngleBrackets ?? string.Empty;
50-
return $"{typeName}{parameterListInAngleBrackets}";
51-
}
5248
}

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,29 @@ namespace M31.FluentApi.Generator.CodeGeneration.CodeBoardElements;
55

66
internal abstract class FluentApiSymbolInfo
77
{
8-
internal FluentApiSymbolInfo(string name, Accessibility accessibility, bool requiresReflection)
8+
internal FluentApiSymbolInfo(
9+
string name,
10+
string declaringClassNameWithTypeParameters,
11+
Accessibility accessibility,
12+
bool requiresReflection)
913
{
1014
Name = name;
1115
NameInCamelCase = Name.TrimStart('_').FirstCharToLower();
16+
DeclaringClassNameWithTypeParameters = declaringClassNameWithTypeParameters;
1217
Accessibility = accessibility;
1318
RequiresReflection = requiresReflection;
1419
}
1520

1621
internal string Name { get; }
1722
internal string NameInCamelCase { get; }
23+
internal string DeclaringClassNameWithTypeParameters { get; }
1824
internal Accessibility Accessibility { get; }
1925
internal bool RequiresReflection { get; }
2026

2127
protected bool Equals(FluentApiSymbolInfo other)
2228
{
2329
return Name == other.Name &&
30+
DeclaringClassNameWithTypeParameters == other.DeclaringClassNameWithTypeParameters &&
2431
Accessibility == other.Accessibility &&
2532
RequiresReflection == other.RequiresReflection;
2633
}
@@ -35,6 +42,6 @@ public override bool Equals(object? obj)
3542

3643
public override int GetHashCode()
3744
{
38-
return new HashCode().Add(Name, Accessibility, RequiresReflection);
45+
return new HashCode().Add(Name, DeclaringClassNameWithTypeParameters, Accessibility, RequiresReflection);
3946
}
4047
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ internal class MemberSymbolInfo : FluentApiSymbolInfo
99
internal MemberSymbolInfo(
1010
string name,
1111
string type,
12+
string declaringClassNameWithTypeParameters,
1213
Accessibility accessibility,
1314
bool requiresReflection,
1415
string typeForCodeGeneration,
1516
bool isNullable,
1617
bool isProperty,
1718
CollectionType? collectionType)
18-
: base(name, accessibility, requiresReflection)
19+
: base(name, declaringClassNameWithTypeParameters, accessibility, requiresReflection)
1920
{
2021
Type = type;
2122
TypeForCodeGeneration = typeForCodeGeneration;

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ internal class MethodSymbolInfo : FluentApiSymbolInfo
88
{
99
internal MethodSymbolInfo(
1010
string name,
11+
string declaringClassNameWithTypeParameters,
1112
Accessibility accessibility,
1213
bool requiresReflection,
1314
GenericInfo? genericInfo,
1415
IReadOnlyCollection<ParameterSymbolInfo> parameterInfos,
1516
string returnType)
16-
: base(name, accessibility, requiresReflection)
17+
: base(name, declaringClassNameWithTypeParameters, accessibility, requiresReflection)
1718
{
1819
GenericInfo = genericInfo;
1920
ParameterInfos = parameterInfos;

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

+1-1
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.8.0</PackageVersion>
14+
<PackageVersion>1.9.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/SourceGenerators/ClassInfoFactory.cs

+65-23
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
5757
bool isStruct = syntaxKind is SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration;
5858

5959
FluentApiClassInfo? classInfo = CreateFluentApiClassInfo(
60-
typeData.Type,
61-
typeData.GenericInfo,
62-
typeData.AttributeData,
63-
typeData.UsingStatements,
60+
typeData,
6461
isStruct,
6562
generatorConfig.NewLineString,
6663
cancellationToken);
@@ -75,35 +72,48 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
7572
}
7673

7774
private FluentApiClassInfo? CreateFluentApiClassInfo(
78-
INamedTypeSymbol type,
79-
GenericInfo? genericInfo,
80-
AttributeDataExtended attributeDataExtended,
81-
IReadOnlyCollection<string> usingStatements,
75+
TypeData typeData,
8276
bool isStruct,
8377
string newLineString,
8478
CancellationToken cancellationToken)
8579
{
86-
string className = type.Name;
87-
string? @namespace = type.ContainingNamespace.IsGlobalNamespace ? null : type.ContainingNamespace.ToString();
88-
bool isInternal = type.DeclaredAccessibility == Accessibility.Internal;
89-
ConstructorInfo? constructorInfo = TryGetConstructorInfo(type);
80+
string className = typeData.Type.Name;
81+
string? @namespace = typeData.Type.ContainingNamespace.IsGlobalNamespace
82+
? null
83+
: typeData.Type.ContainingNamespace.ToString();
84+
bool isInternal = typeData.Type.DeclaredAccessibility == Accessibility.Internal;
85+
ConstructorInfo? constructorInfo = TryGetConstructorInfo(typeData.Type);
9086
FluentApiAttributeInfo fluentApiAttributeInfo =
91-
FluentApiAttributeInfo.Create(attributeDataExtended.AttributeData, className);
87+
FluentApiAttributeInfo.Create(typeData.AttributeDataExtended.AttributeData, className);
9288

9389
List<FluentApiInfo> infos = new List<FluentApiInfo>();
90+
(ITypeSymbol declaringType, ISymbol[] members)[] allMembers =
91+
GetMembersOfTypeAndBaseTypes(typeData.Type).ToArray();
9492

95-
foreach (var member in type.GetMembers().Where(m => m.CanBeReferencedByName && m.Name != string.Empty))
93+
foreach ((ITypeSymbol declaringType, ISymbol[] members) in allMembers)
9694
{
97-
if (cancellationToken.IsCancellationRequested)
95+
if (declaringType is not INamedTypeSymbol namedTypeSymbol)
9896
{
99-
return null;
97+
throw new GenerationException($"The type {declaringType.Name} is not a named type symbol.");
10098
}
10199

102-
FluentApiInfo? fluentApiInfo = TryCreateFluentApiInfo(member);
100+
GenericInfo? genericInfo = GenericInfo.TryCreate(namedTypeSymbol);
101+
string declaringClassNameWithGenericParameters =
102+
AugmentTypeNameWithGenericParameters(namedTypeSymbol.Name, genericInfo);
103103

104-
if (fluentApiInfo != null)
104+
foreach (ISymbol member in members)
105105
{
106-
infos.Add(fluentApiInfo);
106+
if (cancellationToken.IsCancellationRequested)
107+
{
108+
return null;
109+
}
110+
111+
FluentApiInfo? fluentApiInfo = TryCreateFluentApiInfo(member, declaringClassNameWithGenericParameters);
112+
113+
if (fluentApiInfo != null)
114+
{
115+
infos.Add(fluentApiInfo);
116+
}
107117
}
108118
}
109119

@@ -112,17 +122,43 @@ private ClassInfoResult CreateFluentApiClassInfoInternal(
112122
return new FluentApiClassInfo(
113123
className,
114124
@namespace,
115-
genericInfo,
125+
typeData.GenericInfo,
116126
isStruct,
117127
isInternal,
118128
constructorInfo!,
119129
fluentApiAttributeInfo.BuilderClassName,
120130
newLineString,
121131
infos,
122-
usingStatements,
132+
typeData.UsingStatements,
123133
new FluentApiClassAdditionalInfo(groups));
124134
}
125135

136+
private static List<(ITypeSymbol declaringType, ISymbol[] members)> GetMembersOfTypeAndBaseTypes(
137+
ITypeSymbol typeSymbol)
138+
{
139+
List<(ITypeSymbol declaringType, ISymbol[] members)> result =
140+
new List<(ITypeSymbol declaringType, ISymbol[] members)>();
141+
142+
GetMembers(typeSymbol);
143+
return result;
144+
145+
void GetMembers(ITypeSymbol currentTypeSymbol)
146+
{
147+
ISymbol[] members = currentTypeSymbol.GetMembers()
148+
.Where(m => m.CanBeReferencedByName && m.Name != string.Empty).ToArray();
149+
150+
result.Add((currentTypeSymbol, members));
151+
152+
if (currentTypeSymbol.BaseType == null ||
153+
currentTypeSymbol.BaseType.SpecialType == SpecialType.System_Object)
154+
{
155+
return;
156+
}
157+
158+
GetMembers(currentTypeSymbol.BaseType);
159+
}
160+
}
161+
126162
private ConstructorInfo? TryGetConstructorInfo(INamedTypeSymbol type)
127163
{
128164
/* Look for the default constructor. If it is not present, take the constructor
@@ -165,7 +201,7 @@ with the fewest parameters that is explicitly declared. */
165201
constructors[0].DeclaredAccessibility != Accessibility.Public);
166202
}
167203

168-
private FluentApiInfo? TryCreateFluentApiInfo(ISymbol symbol)
204+
private FluentApiInfo? TryCreateFluentApiInfo(ISymbol symbol, string declaringClassNameWithTypeParameters)
169205
{
170206
AttributeDataExtractor extractor = new AttributeDataExtractor(report);
171207
FluentApiAttributeData? attributeData = extractor.GetAttributeData(symbol);
@@ -182,6 +218,12 @@ with the fewest parameters that is explicitly declared. */
182218
}
183219

184220
FluentApiInfoCreator fluentApiInfoCreator = new FluentApiInfoCreator(report);
185-
return fluentApiInfoCreator.Create(symbol, attributeData);
221+
return fluentApiInfoCreator.Create(symbol, attributeData, declaringClassNameWithTypeParameters);
222+
}
223+
224+
public static string AugmentTypeNameWithGenericParameters(string typeName, GenericInfo? genericInfo)
225+
{
226+
string parameterListInAngleBrackets = genericInfo?.ParameterListInAngleBrackets ?? string.Empty;
227+
return $"{typeName}{parameterListInAngleBrackets}";
186228
}
187229
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ internal FluentApiInfoCreator(ClassInfoReport classInfoReport)
1717
this.classInfoReport = classInfoReport;
1818
}
1919

20-
internal FluentApiInfo? Create(ISymbol symbol, FluentApiAttributeData attributeData)
20+
internal FluentApiInfo? Create(
21+
ISymbol symbol,
22+
FluentApiAttributeData attributeData,
23+
string declaringClassNameWithTypeParameters)
2124
{
22-
FluentApiSymbolInfo symbolInfo = SymbolInfoCreator.Create(symbol);
25+
FluentApiSymbolInfo symbolInfo = SymbolInfoCreator.Create(symbol, declaringClassNameWithTypeParameters);
2326
AttributeInfoBase? attributeInfo = CreateAttributeInfo(attributeData.MainAttributeData, symbol, symbolInfo);
2427

2528
if (attributeInfo == null)

src/M31.FluentApi.Generator/SourceGenerators/Generics/GenericInfo.cs

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ private GenericInfo(IReadOnlyCollection<GenericTypeParameter> parameters)
1010
Parameters = parameters;
1111
}
1212

13+
internal static GenericInfo? TryCreate(INamedTypeSymbol type)
14+
{
15+
return type.IsGenericType ? Create(type.TypeParameters) : null;
16+
}
17+
1318
internal static GenericInfo Create(IEnumerable<ITypeParameterSymbol> typeParameters)
1419
{
1520
GenericTypeParameter[] parameters =

0 commit comments

Comments
 (0)