Skip to content

Commit 4943686

Browse files
kimpenhausbuehler
andauthored
feat: support inheritance for controllers and finalizers (#873)
@buehler maybe you can double check this PR if you will find some time. we came accross the issue, that any controller and finalizer need to implement `IEntityController<TEntity>` or corresponding `IEntityFinalizer<TEntity>` to generate the appropriate entries to regiter them in the ServiceCollection. Having as base class like EntityControllerBase<TEntity> / EntityFinalizerBase<TEntity> which implements `IEntity...` wouldn't work. As a workaround I just added a redundant `IEntity...` on the inherited class as well as on the base class. Not the sweetest solution but a working one :) As far as I could see this was due to the pure textual syntax analysis which just checked for the baselist-types. I've changed this to walk the semantic syntax model and support inheritance upon multi-levels. thanks in advance. --------- Co-authored-by: Christoph Bühler <[email protected]>
1 parent 994dc37 commit 4943686

11 files changed

+272
-46
lines changed
Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
using Microsoft.CodeAnalysis;
22
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
34

45
namespace KubeOps.Generator.Generators;
56

67
public static class AutoGeneratedSyntaxTrivia
78
{
8-
public static readonly SyntaxTrivia Instance =
9-
SyntaxFactory.Comment(
10-
"""
11-
// <auto-generated>
12-
// This code was generated by a tool.
13-
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
14-
// </auto-generated>
15-
""");
9+
public static readonly SyntaxTriviaList Instance =
10+
new(
11+
SyntaxFactory.Comment(
12+
"""
13+
// <auto-generated>
14+
// This code was generated by a tool.
15+
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
16+
// </auto-generated>
17+
"""),
18+
SyntaxFactory.CarriageReturnLineFeed,
19+
SyntaxFactory.Trivia(
20+
SyntaxFactory.PragmaWarningDirectiveTrivia(
21+
SyntaxFactory.Token(SyntaxKind.DisableKeyword),
22+
SyntaxFactory.SeparatedList<ExpressionSyntax>(new SyntaxNodeOrTokenList(SyntaxFactory.IdentifierName("CS1591"))),
23+
false)),
24+
SyntaxFactory.CarriageReturnLineFeed);
1625
}

src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace KubeOps.Generator.SyntaxReceiver;
44

5-
internal class CombinedSyntaxReceiver(params ISyntaxContextReceiver[] receivers) : ISyntaxContextReceiver
5+
internal sealed class CombinedSyntaxReceiver(params ISyntaxContextReceiver[] receivers) : ISyntaxContextReceiver
66
{
77
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
88
{

src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,45 @@
33

44
namespace KubeOps.Generator.SyntaxReceiver;
55

6-
internal class EntityControllerSyntaxReceiver : ISyntaxContextReceiver
6+
internal sealed class EntityControllerSyntaxReceiver : ISyntaxContextReceiver
77
{
8+
private const string IEntityControllerMetadataName = "KubeOps.Abstractions.Controller.IEntityController`1";
9+
810
public List<(ClassDeclarationSyntax Controller, string EntityName)> Controllers { get; } = [];
911

1012
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
1113
{
12-
if (context.Node is not ClassDeclarationSyntax { BaseList.Types.Count: > 0 } cls ||
13-
cls.BaseList.Types.FirstOrDefault(t => t is
14-
{ Type: GenericNameSyntax { Identifier.Text: "IEntityController" } }) is not { } baseType)
14+
if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax)
15+
{
16+
return;
17+
}
18+
19+
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol)
20+
{
21+
return;
22+
}
23+
24+
if (classSymbol.IsAbstract)
25+
{
26+
return;
27+
}
28+
29+
var iEntityControllerInterface = context.SemanticModel.Compilation.GetTypeByMetadataName(IEntityControllerMetadataName);
30+
if (iEntityControllerInterface is null)
31+
{
32+
return;
33+
}
34+
35+
var implementedControllerInterface = classSymbol.AllInterfaces
36+
.FirstOrDefault(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iEntityControllerInterface));
37+
38+
var entityTypeSymbol = implementedControllerInterface?.TypeArguments.FirstOrDefault();
39+
40+
if (entityTypeSymbol == null)
1541
{
1642
return;
1743
}
1844

19-
var targetEntity = (baseType.Type as GenericNameSyntax)!.TypeArgumentList.Arguments.First();
20-
Controllers.Add((cls, targetEntity.ToString()));
45+
Controllers.Add((classDeclarationSyntax, entityTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
2146
}
2247
}

src/KubeOps.Generator/SyntaxReceiver/EntityFinalizerSyntaxReceiver.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,45 @@
33

44
namespace KubeOps.Generator.SyntaxReceiver;
55

6-
internal class EntityFinalizerSyntaxReceiver : ISyntaxContextReceiver
6+
internal sealed class EntityFinalizerSyntaxReceiver : ISyntaxContextReceiver
77
{
8+
private const string IEntityFinalizerMetadataName = "KubeOps.Abstractions.Controller.IEntityFinalizer`1";
9+
810
public List<(ClassDeclarationSyntax Finalizer, string EntityName)> Finalizer { get; } = [];
911

1012
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
1113
{
12-
if (context.Node is not ClassDeclarationSyntax { BaseList.Types.Count: > 0 } cls ||
13-
cls.BaseList.Types.FirstOrDefault(t => t is
14-
{ Type: GenericNameSyntax { Identifier.Text: "IEntityFinalizer" } }) is not { } baseType)
14+
if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax)
15+
{
16+
return;
17+
}
18+
19+
if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol)
20+
{
21+
return;
22+
}
23+
24+
if (classSymbol.IsAbstract)
25+
{
26+
return;
27+
}
28+
29+
var iEntityFinalizerInterface = context.SemanticModel.Compilation.GetTypeByMetadataName(IEntityFinalizerMetadataName);
30+
if (iEntityFinalizerInterface is null)
31+
{
32+
return;
33+
}
34+
35+
var implementedEntityFinalizerInterface = classSymbol.AllInterfaces
36+
.FirstOrDefault(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iEntityFinalizerInterface));
37+
38+
var entityTypeSymbol = implementedEntityFinalizerInterface?.TypeArguments.FirstOrDefault();
39+
40+
if (entityTypeSymbol == null)
1541
{
1642
return;
1743
}
1844

19-
var targetEntity = (baseType.Type as GenericNameSyntax)!.TypeArgumentList.Arguments.First();
20-
Finalizer.Add((cls, targetEntity.ToString()));
45+
Finalizer.Add((classDeclarationSyntax, entityTypeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat)));
2146
}
2247
}

src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace KubeOps.Generator.SyntaxReceiver;
55

6-
internal class KubernetesEntitySyntaxReceiver : ISyntaxContextReceiver
6+
internal sealed class KubernetesEntitySyntaxReceiver : ISyntaxContextReceiver
77
{
88
private const string KindName = "Kind";
99
private const string GroupName = "Group";

test/KubeOps.Generator.Test/ControllerRegistrationGenerator.Test.cs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
1-
using FluentAssertions;
1+
using System.Collections.Immutable;
2+
3+
using FluentAssertions;
24

35
using KubeOps.Generator.Generators;
46

7+
using Microsoft.CodeAnalysis;
58
using Microsoft.CodeAnalysis.CSharp;
69

710
namespace KubeOps.Generator.Test;
811

9-
public class ControllerRegistrationGeneratorTest
12+
public sealed class ControllerRegistrationGeneratorTest
1013
{
1114
[Theory]
1215
[InlineData("", """
1316
// <auto-generated>
1417
// This code was generated by a tool.
1518
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
1619
// </auto-generated>
20+
#pragma warning disable CS1591
1721
using KubeOps.Abstractions.Builder;
1822
1923
public static class ControllerRegistrations
@@ -25,6 +29,10 @@ public static IOperatorBuilder RegisterControllers(this IOperatorBuilder builder
2529
}
2630
""")]
2731
[InlineData("""
32+
using k8s;
33+
using k8s.Models;
34+
using KubeOps.Abstractions.Controller;
35+
2836
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
2937
public class V1TestEntity : IKubernetesObject<V1ObjectMeta>
3038
{
@@ -38,6 +46,42 @@ public class V1TestEntityController : IEntityController<V1TestEntity>
3846
// This code was generated by a tool.
3947
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
4048
// </auto-generated>
49+
#pragma warning disable CS1591
50+
using KubeOps.Abstractions.Builder;
51+
52+
public static class ControllerRegistrations
53+
{
54+
public static IOperatorBuilder RegisterControllers(this IOperatorBuilder builder)
55+
{
56+
builder.AddController<global::V1TestEntityController, global::V1TestEntity>();
57+
return builder;
58+
}
59+
}
60+
""")]
61+
[InlineData("""
62+
using k8s;
63+
using k8s.Models;
64+
using KubeOps.Abstractions.Controller;
65+
66+
[KubernetesEntity(Group = "testing.dev", ApiVersion = "v1", Kind = "TestEntity")]
67+
public sealed class V1TestEntity : IKubernetesObject<V1ObjectMeta>
68+
{
69+
}
70+
71+
public abstract class EntityControllerBase<TEntity> : IEntityController<TEntity>
72+
where TEntity : IKubernetesObject<V1ObjectMeta>
73+
{
74+
}
75+
76+
public sealed class V1TestEntityController : EntityControllerBase<V1TestEntity>
77+
{
78+
}
79+
""", """
80+
// <auto-generated>
81+
// This code was generated by a tool.
82+
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
83+
// </auto-generated>
84+
#pragma warning disable CS1591
4185
using KubeOps.Abstractions.Builder;
4286
4387
public static class ControllerRegistrations
@@ -55,7 +99,7 @@ public void Should_Generate_Correct_Code(string input, string expectedResult)
5599
expectedResult = expectedResult.ReplaceLineEndings();
56100

57101
var driver = CSharpGeneratorDriver.Create(new ControllerRegistrationGenerator());
58-
driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag);
102+
driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out ImmutableArray<Diagnostic> _);
59103

60104
var result = output.SyntaxTrees
61105
.First(s => s.FilePath.Contains("ControllerRegistrations.g.cs"))

test/KubeOps.Generator.Test/EntityDefinitionGenerator.Test.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
using FluentAssertions;
1+
using System.Collections.Immutable;
2+
3+
using FluentAssertions;
24

35
using KubeOps.Generator.Generators;
46

7+
using Microsoft.CodeAnalysis;
58
using Microsoft.CodeAnalysis.CSharp;
69

710
namespace KubeOps.Generator.Test;
@@ -14,6 +17,7 @@ public class EntityDefinitionGeneratorTest
1417
// This code was generated by a tool.
1518
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
1619
// </auto-generated>
20+
#pragma warning disable CS1591
1721
using KubeOps.Abstractions.Builder;
1822
using KubeOps.Abstractions.Entities;
1923
@@ -31,6 +35,7 @@ public class V1TestEntity : IKubernetesObject<V1ObjectMeta>
3135
// This code was generated by a tool.
3236
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
3337
// </auto-generated>
38+
#pragma warning disable CS1591
3439
using KubeOps.Abstractions.Builder;
3540
using KubeOps.Abstractions.Entities;
3641
@@ -54,6 +59,7 @@ public class V1AnotherEntity : IKubernetesObject<V1ObjectMeta>
5459
// This code was generated by a tool.
5560
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
5661
// </auto-generated>
62+
#pragma warning disable CS1591
5763
using KubeOps.Abstractions.Builder;
5864
using KubeOps.Abstractions.Entities;
5965
@@ -69,7 +75,7 @@ public void Should_Generate_Correct_Code(string input, string expectedResult)
6975
expectedResult = expectedResult.ReplaceLineEndings();
7076

7177
var driver = CSharpGeneratorDriver.Create(new EntityDefinitionGenerator());
72-
driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag);
78+
driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out ImmutableArray<Diagnostic> _);
7379

7480
var result = output.SyntaxTrees
7581
.First(s => s.FilePath.Contains("EntityDefinitions.g.cs"))

test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
using FluentAssertions;
1+
using System.Collections.Immutable;
2+
3+
using FluentAssertions;
24

35
using KubeOps.Generator.Generators;
46

7+
using Microsoft.CodeAnalysis;
58
using Microsoft.CodeAnalysis.CSharp;
69

710
namespace KubeOps.Generator.Test;
@@ -17,6 +20,7 @@ public void Should_Generate_Empty_Initializer_Without_Input()
1720
// This code was generated by a tool.
1821
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
1922
// </auto-generated>
23+
#pragma warning disable CS1591
2024
public static class EntityInitializer
2125
{
2226
}
@@ -50,6 +54,7 @@ public class V2TestEntity : IKubernetesObject<V1ObjectMeta>
5054
// This code was generated by a tool.
5155
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
5256
// </auto-generated>
57+
#pragma warning disable CS1591
5358
public static class EntityInitializer
5459
{
5560
public static global::V1TestEntity Initialize(this global::V1TestEntity entity)
@@ -93,6 +98,7 @@ public class V1ConfigMap : IKubernetesObject<V1ObjectMeta>
9398
// This code was generated by a tool.
9499
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
95100
// </auto-generated>
101+
#pragma warning disable CS1591
96102
public static class EntityInitializer
97103
{
98104
public static global::V1ConfigMap Initialize(this global::V1ConfigMap entity)
@@ -129,6 +135,7 @@ public V1TestEntity(){}
129135
// This code was generated by a tool.
130136
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
131137
// </auto-generated>
138+
#pragma warning disable CS1591
132139
public static class EntityInitializer
133140
{
134141
public static global::V1TestEntity Initialize(this global::V1TestEntity entity)
@@ -164,6 +171,7 @@ public partial class V1TestEntity : IKubernetesObject<V1ObjectMeta>
164171
// This code was generated by a tool.
165172
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
166173
// </auto-generated>
174+
#pragma warning disable CS1591
167175
public static class EntityInitializer
168176
{
169177
}
@@ -194,6 +202,7 @@ public partial class V1TestEntity : IKubernetesObject<V1ObjectMeta>
194202
// This code was generated by a tool.
195203
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
196204
// </auto-generated>
205+
#pragma warning disable CS1591
197206
namespace Foo.Bar;
198207
public partial class V1TestEntity
199208
{
@@ -234,6 +243,7 @@ public partial class V1TestEntity : IKubernetesObject<V1ObjectMeta>
234243
// This code was generated by a tool.
235244
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
236245
// </auto-generated>
246+
#pragma warning disable CS1591
237247
namespace Foo.Bar.Baz;
238248
public partial class V1TestEntity
239249
{
@@ -268,6 +278,7 @@ public partial class V1TestEntity : IKubernetesObject<V1ObjectMeta>
268278
// This code was generated by a tool.
269279
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
270280
// </auto-generated>
281+
#pragma warning disable CS1591
271282
public partial class V1TestEntity
272283
{
273284
public V1TestEntity()
@@ -302,6 +313,7 @@ public V1TestEntity(string name){}
302313
// This code was generated by a tool.
303314
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
304315
// </auto-generated>
316+
#pragma warning disable CS1591
305317
public partial class V1TestEntity
306318
{
307319
public V1TestEntity()
@@ -313,7 +325,7 @@ public V1TestEntity()
313325
""".ReplaceLineEndings();
314326

315327
var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator());
316-
driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out var diag);
328+
driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out ImmutableArray<Diagnostic> _);
317329

318330
var result = output.SyntaxTrees
319331
.First(s => s.FilePath.Contains("V1TestEntity.init.g.cs"))

0 commit comments

Comments
 (0)