From 0605533ab3ff30470953b20ae42cd29da71259d1 Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Fri, 12 Jun 2026 12:01:02 +0200 Subject: [PATCH 1/4] refactor(generator): update ISourceGenerator to IIncrementalGenerator --- .../ControllerRegistrationGenerator.cs | 33 +- .../Generators/EntityDefinitionGenerator.cs | 15 +- .../Generators/EntityInitializerGenerator.cs | 19 +- .../FinalizerRegistrationGenerator.cs | 41 ++- .../Generators/OperatorBuilderGenerator.cs | 7 +- .../KubeOps.Generator.csproj | 4 +- .../ClassDeclarationMetaData.cs | 4 +- .../SyntaxReceiver/CombinedSyntaxReceiver.cs | 18 -- .../EntityControllerSyntaxReceiver.cs | 60 ---- .../SyntaxReceiver/EntityDiscovery.cs | 282 ++++++++++++++++++ .../EntityFinalizerSyntaxReceiver.cs | 60 ---- .../SyntaxReceiver/EquatableArray.cs | 91 ++++++ .../KubernetesEntitySyntaxReceiver.cs | 186 ------------ 13 files changed, 422 insertions(+), 398 deletions(-) delete mode 100644 src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs delete mode 100644 src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs create mode 100644 src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs delete mode 100644 src/KubeOps.Generator/SyntaxReceiver/EntityFinalizerSyntaxReceiver.cs create mode 100644 src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs delete mode 100644 src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs diff --git a/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs b/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs index 005106063..7bf467a29 100644 --- a/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs +++ b/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs @@ -16,23 +16,20 @@ namespace KubeOps.Generator.Generators; [Generator] -internal sealed class ControllerRegistrationGenerator : ISourceGenerator +internal sealed class ControllerRegistrationGenerator : IIncrementalGenerator { - private readonly EntityControllerSyntaxReceiver _ctrlReceiver = new(); - private readonly KubernetesEntitySyntaxReceiver _entityReceiver = new(); - - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new CombinedSyntaxReceiver(_ctrlReceiver, _entityReceiver)); + context.RegisterSourceOutput( + EntityDiscovery.GetControllers(context).Combine(EntityDiscovery.GetEntities(context)), + (spc, source) => Execute(spc, source.Left, source.Right)); } - public void Execute(GeneratorExecutionContext context) + private static void Execute( + SourceProductionContext context, + EquatableArray controllers, + EquatableArray entities) { - if (context.SyntaxContextReceiver is not CombinedSyntaxReceiver) - { - return; - } - var declaration = CompilationUnit() .WithUsings( List( @@ -57,11 +54,11 @@ public void Execute(GeneratorExecutionContext context) .WithType( IdentifierName("IOperatorBuilder"))))) .WithBody(Block( - _ctrlReceiver.Controllers - .Where(c => _entityReceiver.Entities.Exists(e => + controllers + .Where(c => entities.Any(e => e.ClassDeclaration.FullyQualifiedName == c.FullyQualifiedEntityName)) .OrderBy(c => c.FullyQualifiedEntityName, StringComparer.Ordinal) - .Select(c => (c.Controller, Entity: _entityReceiver.Entities.First(e => + .Select(c => (c.FullyQualifiedController, Entity: entities.First(e => e.ClassDeclaration.FullyQualifiedName == c.FullyQualifiedEntityName))) .Select(e => ExpressionStatement( InvocationExpression( @@ -73,11 +70,7 @@ public void Execute(GeneratorExecutionContext context) TypeArgumentList( SeparatedList(new[] { - IdentifierName(context.Compilation - .GetSemanticModel(e.Controller.SyntaxTree) - .GetDeclaredSymbol(e.Controller)! - .ToDisplayString(SymbolDisplayFormat - .FullyQualifiedFormat)), + IdentifierName(e.FullyQualifiedController), IdentifierName(e.Entity.ClassDeclaration.FullyQualifiedName), }))))))) .Append(ReturnStatement(IdentifierName("builder")))))))) diff --git a/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs b/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs index 1f418d52d..af6767914 100644 --- a/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs +++ b/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs @@ -16,20 +16,15 @@ namespace KubeOps.Generator.Generators; [Generator] -internal sealed class EntityDefinitionGenerator : ISourceGenerator +internal sealed class EntityDefinitionGenerator : IIncrementalGenerator { - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new KubernetesEntitySyntaxReceiver()); + context.RegisterSourceOutput(EntityDiscovery.GetEntities(context), Execute); } - public void Execute(GeneratorExecutionContext context) + private static void Execute(SourceProductionContext context, EquatableArray entities) { - if (context.SyntaxContextReceiver is not KubernetesEntitySyntaxReceiver receiver) - { - return; - } - var declaration = CompilationUnit() .WithUsings( List( @@ -43,7 +38,7 @@ public void Execute(GeneratorExecutionContext context) .WithModifiers(TokenList( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) .WithMembers( - List(receiver.Entities + List(entities .OrderBy(e => e.ClassDeclaration.FullyQualifiedName, StringComparer.Ordinal) .Select(e => FieldDeclaration( VariableDeclaration( diff --git a/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs b/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs index ff2c58d7b..87ca1e10a 100644 --- a/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs +++ b/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs @@ -16,26 +16,21 @@ namespace KubeOps.Generator.Generators; [Generator] -internal sealed class EntityInitializerGenerator : ISourceGenerator +internal sealed class EntityInitializerGenerator : IIncrementalGenerator { private const string EntityIdentifier = "entity"; - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new KubernetesEntitySyntaxReceiver()); + context.RegisterSourceOutput(EntityDiscovery.GetEntities(context), Execute); } - public void Execute(GeneratorExecutionContext context) + private static void Execute(SourceProductionContext context, EquatableArray entities) { - if (context.SyntaxContextReceiver is not KubernetesEntitySyntaxReceiver receiver) - { - return; - } - // for each partial defined entity, create a partial class that // introduces a default constructor that initializes the ApiVersion and Kind. // But only, if there is no default constructor defined. - foreach (var entity in receiver.Entities + foreach (var entity in entities .Where(e => e.ClassDeclaration is { IsFromReferencedAssembly: false, IsPartial: true, HasParameterlessConstructor: false }) .OrderBy(e => e.ClassDeclaration.FullyQualifiedName, StringComparer.Ordinal)) { @@ -56,7 +51,7 @@ public void Execute(GeneratorExecutionContext context) partialEntityInitializer = partialEntityInitializer .AddMembers(ClassDeclaration(entity.ClassDeclaration.ClassName) - .WithModifiers(entity.ClassDeclaration.Modifiers!.Value) + .WithModifiers(TokenList(entity.ClassDeclaration.ModifierKinds.Select(k => Token(k)))) .AddMembers(ConstructorDeclaration(entity.ClassDeclaration.ClassName) .WithModifiers( TokenList( @@ -98,7 +93,7 @@ public void Execute(GeneratorExecutionContext context) .WithMembers(SingletonList(ClassDeclaration("EntityInitializer") .WithModifiers(TokenList( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) - .WithMembers(List(receiver.Entities + .WithMembers(List(entities .Where(e => e.ClassDeclaration.IsFromReferencedAssembly || !e.ClassDeclaration.IsPartial || e.ClassDeclaration.HasParameterlessConstructor) .OrderBy(e => e.ClassDeclaration.FullyQualifiedName, StringComparer.Ordinal) .Select(e => (Entity: e, ClassIdentifier: e.ClassDeclaration.FullyQualifiedName)) diff --git a/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs b/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs index 352bc9f4e..9893adfbe 100644 --- a/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs +++ b/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs @@ -16,30 +16,27 @@ namespace KubeOps.Generator.Generators; [Generator] -internal sealed class FinalizerRegistrationGenerator : ISourceGenerator +internal sealed class FinalizerRegistrationGenerator : IIncrementalGenerator { private const byte MaxNameLength = 63; - private readonly EntityFinalizerSyntaxReceiver _finalizerReceiver = new(); - private readonly KubernetesEntitySyntaxReceiver _entityReceiver = new(); - - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new CombinedSyntaxReceiver(_finalizerReceiver, _entityReceiver)); + context.RegisterSourceOutput( + EntityDiscovery.GetFinalizers(context).Combine(EntityDiscovery.GetEntities(context)), + (spc, source) => Execute(spc, source.Left, source.Right)); } - public void Execute(GeneratorExecutionContext context) + private static void Execute( + SourceProductionContext context, + EquatableArray finalizerRegistrations, + EquatableArray entities) { - if (context.SyntaxContextReceiver is not CombinedSyntaxReceiver) - { - return; - } - - var finalizers = _finalizerReceiver.Finalizer - .Where(c => _entityReceiver.Entities.Exists(e => + var finalizers = finalizerRegistrations + .Where(c => entities.Any(e => e.ClassDeclaration.FullyQualifiedName == c.FullyQualifiedEntityName)) .OrderBy(c => c.FullyQualifiedEntityName, StringComparer.Ordinal) - .Select(c => (c.Finalizer, Entity: _entityReceiver.Entities.First(e => + .Select(c => (c.FullyQualifiedFinalizer, c.IdentifierName, Entity: entities.First(e => e.ClassDeclaration.FullyQualifiedName == c.FullyQualifiedEntityName))).ToList(); var declaration = CompilationUnit() @@ -57,7 +54,7 @@ public void Execute(GeneratorExecutionContext context) Token(SyntaxKind.StringKeyword))) .WithVariables( SingletonSeparatedList( - VariableDeclarator(Identifier($"{f.Finalizer.Identifier}Identifier")) + VariableDeclarator(Identifier($"{f.IdentifierName}Identifier")) .WithInitializer( EqualsValueClause( LiteralExpression( @@ -88,18 +85,14 @@ public void Execute(GeneratorExecutionContext context) TypeArgumentList( SeparatedList(new[] { - IdentifierName(context.Compilation - .GetSemanticModel(f.Finalizer.SyntaxTree) - .GetDeclaredSymbol(f.Finalizer)! - .ToDisplayString(SymbolDisplayFormat - .FullyQualifiedFormat)), + IdentifierName(f.FullyQualifiedFinalizer), IdentifierName(f.Entity.ClassDeclaration.FullyQualifiedName), }))))) .WithArgumentList( ArgumentList( SingletonSeparatedList( Argument( - IdentifierName($"{f.Finalizer.Identifier}Identifier"))))))) + IdentifierName($"{f.IdentifierName}Identifier"))))))) .Append(ReturnStatement(IdentifierName("builder")))))))) .NormalizeWhitespace(); @@ -108,9 +101,9 @@ public void Execute(GeneratorExecutionContext context) SourceText.From(declaration.ToFullString(), Encoding.UTF8, SourceHashAlgorithm.Sha256)); } - private static string FinalizerName((ClassDeclarationSyntax Finalizer, AttributedEntity Entity) finalizer) + private static string FinalizerName((string FullyQualifiedFinalizer, string IdentifierName, AttributedEntity Entity) finalizer) { - var finalizerName = finalizer.Finalizer.Identifier.ToString().ToLowerInvariant(); + var finalizerName = finalizer.IdentifierName.ToLowerInvariant(); var name = $"{finalizer.Entity.Group}/{finalizerName}{(finalizerName.EndsWith("finalizer") ? string.Empty : "finalizer")}" .TrimStart('/'); diff --git a/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs b/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs index 3acb31de4..de630429b 100644 --- a/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs +++ b/src/KubeOps.Generator/Generators/OperatorBuilderGenerator.cs @@ -14,15 +14,16 @@ namespace KubeOps.Generator.Generators; [Generator] -internal sealed class OperatorBuilderGenerator : ISourceGenerator +internal sealed class OperatorBuilderGenerator : IIncrementalGenerator { private const string BuilderIdentifier = "builder"; - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { + context.RegisterPostInitializationOutput(Execute); } - public void Execute(GeneratorExecutionContext context) + private static void Execute(IncrementalGeneratorPostInitializationContext context) { var declaration = CompilationUnit() .WithUsings( diff --git a/src/KubeOps.Generator/KubeOps.Generator.csproj b/src/KubeOps.Generator/KubeOps.Generator.csproj index 71607b860..cf9ef7828 100644 --- a/src/KubeOps.Generator/KubeOps.Generator.csproj +++ b/src/KubeOps.Generator/KubeOps.Generator.csproj @@ -19,9 +19,7 @@ - + diff --git a/src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs b/src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs index 32102f0f2..781b3cb10 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace KubeOps.Generator.SyntaxReceiver; @@ -10,7 +10,7 @@ internal record struct ClassDeclarationMetaData( string ClassName, string FullyQualifiedName, string? Namespace, - SyntaxTokenList? Modifiers, + EquatableArray ModifierKinds, bool IsPartial, bool HasParameterlessConstructor, bool IsFromReferencedAssembly); diff --git a/src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs deleted file mode 100644 index d10d137f5..000000000 --- a/src/KubeOps.Generator/SyntaxReceiver/CombinedSyntaxReceiver.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis; - -namespace KubeOps.Generator.SyntaxReceiver; - -internal sealed class CombinedSyntaxReceiver(params ISyntaxContextReceiver[] receivers) : ISyntaxContextReceiver -{ - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - foreach (var syntaxContextReceiver in receivers) - { - syntaxContextReceiver.OnVisitSyntaxNode(context); - } - } -} diff --git a/src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs deleted file mode 100644 index f43fe5eaa..000000000 --- a/src/KubeOps.Generator/SyntaxReceiver/EntityControllerSyntaxReceiver.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.SyntaxReceiver; - -internal sealed class EntityControllerSyntaxReceiver : ISyntaxContextReceiver -{ - private const string IEntityControllerMetadataName = "KubeOps.Abstractions.Reconciliation.Controller.IEntityController`1"; - -#pragma warning disable RS1024 - private readonly HashSet _visitedTypeSymbols = new(SymbolEqualityComparer.Default); -#pragma warning restore RS1024 - - public List<(ClassDeclarationSyntax Controller, string FullyQualifiedEntityName)> Controllers { get; } = []; - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax) - { - return; - } - - if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) - { - return; - } - - if (!_visitedTypeSymbols.Add(classSymbol)) - { - return; - } - - if (classSymbol.IsAbstract) - { - return; - } - - var iEntityControllerInterface = context.SemanticModel.Compilation.GetTypeByMetadataName(IEntityControllerMetadataName); - if (iEntityControllerInterface is null) - { - return; - } - - var implementedControllerInterface = classSymbol.AllInterfaces - .FirstOrDefault(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iEntityControllerInterface)); - - var entityTypeSymbol = implementedControllerInterface?.TypeArguments.FirstOrDefault(); - - if (entityTypeSymbol == null) - { - return; - } - - Controllers.Add((classDeclarationSyntax, entityTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); - } -} diff --git a/src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs b/src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs new file mode 100644 index 000000000..e4a1ac4de --- /dev/null +++ b/src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace KubeOps.Generator.SyntaxReceiver; + +internal record struct ControllerRegistration( + string FullyQualifiedController, + string FullyQualifiedEntityName); + +internal record struct FinalizerRegistration( + string FullyQualifiedFinalizer, + string IdentifierName, + string FullyQualifiedEntityName); + +internal static class EntityDiscovery +{ + private const string KubernetesEntitySyntaxName = "KubernetesEntity"; + private const string KubernetesEntityAttributeName = "KubernetesEntityAttribute"; + private const string IEntityControllerMetadataName = "KubeOps.Abstractions.Reconciliation.Controller.IEntityController`1"; + private const string IEntityFinalizerMetadataName = "KubeOps.Abstractions.Reconciliation.Finalizer.IEntityFinalizer`1"; + + private const string KindName = "Kind"; + private const string GroupName = "Group"; + private const string PluralName = "Plural"; + private const string VersionName = "ApiVersion"; + private const string DefaultVersion = "v1"; + + public static IncrementalValueProvider> GetEntities( + IncrementalGeneratorInitializationContext context) + { + var localEntities = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + transform: static (ctx, _) => GetLocalEntity(ctx)) + .Where(static e => e is not null) + .Select(static (e, _) => e!.Value); + + var referencedEntities = context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax { BaseList: not null }, + transform: static (ctx, _) => GetReferencedEntities(ctx)) + .Where(static a => !a.IsDefaultOrEmpty); + + return localEntities.Collect() + .Combine(referencedEntities.Collect()) + .Select(static (pair, _) => Merge(pair.Left, pair.Right)); + } + + public static IncrementalValueProvider> GetControllers( + IncrementalGeneratorInitializationContext context) + => context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax { BaseList: not null }, + transform: static (ctx, _) => GetController(ctx)) + .Where(static c => c is not null) + .Select(static (c, _) => c!.Value) + .Collect() + .Select(static (arr, _) => new EquatableArray(arr.Distinct().ToImmutableArray())); + + public static IncrementalValueProvider> GetFinalizers( + IncrementalGeneratorInitializationContext context) + => context.SyntaxProvider.CreateSyntaxProvider( + predicate: static (node, _) => node is ClassDeclarationSyntax { BaseList: not null }, + transform: static (ctx, _) => GetFinalizer(ctx)) + .Where(static f => f is not null) + .Select(static (f, _) => f!.Value) + .Collect() + .Select(static (arr, _) => new EquatableArray(arr.Distinct().ToImmutableArray())); + + private static EquatableArray Merge( + ImmutableArray local, + ImmutableArray> referenced) + { + // Among entities discovered via usage, the first occurrence per type wins. + var map = referenced + .SelectMany(group => group) + .GroupBy(e => e.ClassDeclaration.FullyQualifiedName, StringComparer.Ordinal) + .ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal); + + // Locally attributed entities take precedence over entities discovered via usage. + foreach (var entity in local) + { + map[entity.ClassDeclaration.FullyQualifiedName] = entity; + } + + var ordered = map.Values + .OrderBy(e => e.ClassDeclaration.FullyQualifiedName, StringComparer.Ordinal) + .ToImmutableArray(); + + return new EquatableArray(ordered); + } + + private static AttributedEntity? GetLocalEntity(GeneratorSyntaxContext context) + { + if (context.Node is not ClassDeclarationSyntax cls) + { + return null; + } + + // Match the attribute syntactically (by name) so entities are discovered even when the + // KubernetesEntity attribute does not bind to a symbol in the input compilation. + var attr = cls.AttributeLists + .SelectMany(a => a.Attributes) + .FirstOrDefault(a => a.Name.ToString() == KubernetesEntitySyntaxName); + + if (attr is null || + ModelExtensions.GetDeclaredSymbol(context.SemanticModel, cls) is not INamedTypeSymbol symbol) + { + return null; + } + + return new AttributedEntity( + new ClassDeclarationMetaData( + ClassName: cls.Identifier.Text, + FullyQualifiedName: symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + Namespace: symbol.ContainingNamespace.IsGlobalNamespace + ? null + : symbol.ContainingNamespace.ToDisplayString(), + ModifierKinds: new EquatableArray( + cls.Modifiers.Select(m => m.Kind()).ToImmutableArray()), + IsPartial: cls.Modifiers.Any(SyntaxKind.PartialKeyword), + HasParameterlessConstructor: cls.Members.Any(m + => m is ConstructorDeclarationSyntax { ParameterList.Parameters.Count: 0 }), + IsFromReferencedAssembly: false), + Kind: GetArgumentValue(context.SemanticModel, attr, KindName) ?? cls.Identifier.Text, + Version: GetArgumentValue(context.SemanticModel, attr, VersionName) ?? DefaultVersion, + Group: GetArgumentValue(context.SemanticModel, attr, GroupName), + Plural: GetArgumentValue(context.SemanticModel, attr, PluralName)); + } + + private static string? GetArgumentValue(SemanticModel model, AttributeSyntax attr, string argName) + { + var expr = attr.ArgumentList?.Arguments + .FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName)?.Expression; + + if (expr is null) + { + return null; + } + + if (model.GetConstantValue(expr) is { HasValue: true, Value: string s }) + { + return s; + } + + return expr is LiteralExpressionSyntax literal ? literal.Token.ValueText : null; + } + + private static ImmutableArray GetReferencedEntities(GeneratorSyntaxContext context) + { + if (context.Node is not ClassDeclarationSyntax classDecl || + ModelExtensions.GetDeclaredSymbol(context.SemanticModel, classDecl) is not INamedTypeSymbol symbol) + { + return ImmutableArray.Empty; + } + + var builder = ImmutableArray.CreateBuilder(); + foreach (var @interface in symbol.AllInterfaces.Where(i => + (i.Name is "IEntityController" or "IEntityFinalizer") + && i is { IsGenericType: true, TypeArguments.Length: > 0 })) + { + if (@interface.TypeArguments[0] is not INamedTypeSymbol entityType) + { + continue; + } + + var entity = BuildEntityFromSymbol(entityType); + if (entity is not null) + { + builder.Add(entity.Value); + } + } + + return builder.ToImmutable(); + } + + private static AttributedEntity? BuildEntityFromSymbol(INamedTypeSymbol symbol) + { + var attr = symbol + .GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.Name == KubernetesEntityAttributeName); + + if (attr is null) + { + return null; + } + + return new AttributedEntity( + new ClassDeclarationMetaData( + ClassName: symbol.Name, + FullyQualifiedName: symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + Namespace: symbol.ContainingNamespace.IsGlobalNamespace + ? null + : symbol.ContainingNamespace.ToDisplayString(), + ModifierKinds: EquatableArray.Empty, + IsPartial: false, + HasParameterlessConstructor: symbol.Constructors + .Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public), + IsFromReferencedAssembly: true), + Kind: GetAttributeValue(attr, KindName) ?? symbol.Name, + Version: GetAttributeValue(attr, VersionName) ?? DefaultVersion, + Group: GetAttributeValue(attr, GroupName), + Plural: GetAttributeValue(attr, PluralName)); + } + + private static ControllerRegistration? GetController(GeneratorSyntaxContext context) + { + var entity = GetImplementedEntity(context, IEntityControllerMetadataName, out var classSymbol); + return entity is null + ? null + : new ControllerRegistration( + classSymbol!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + entity); + } + + private static FinalizerRegistration? GetFinalizer(GeneratorSyntaxContext context) + { + var entity = GetImplementedEntity(context, IEntityFinalizerMetadataName, out var classSymbol); + return entity is null + ? null + : new FinalizerRegistration( + classSymbol!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + classSymbol.Name, + entity); + } + + private static string? GetImplementedEntity( + GeneratorSyntaxContext context, + string interfaceMetadataName, + out INamedTypeSymbol? classSymbol) + { + classSymbol = null; + + if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax || + context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not { } symbol || + symbol.IsAbstract) + { + return null; + } + + var interfaceSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName(interfaceMetadataName); + if (interfaceSymbol is null) + { + return null; + } + + var implemented = symbol.AllInterfaces.FirstOrDefault(i => + i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, interfaceSymbol)); + + if (implemented?.TypeArguments.FirstOrDefault() is not { } entityTypeSymbol) + { + return null; + } + + classSymbol = symbol; + return entityTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + private static string? GetAttributeValue(AttributeData attr, string argName) + { + var namedArg = attr.NamedArguments.FirstOrDefault(a => a.Key == argName); + if (namedArg.Value.Value is string s) + { + return s; + } + + var param = attr.AttributeConstructor?.Parameters + .Select((p, i) => (p, i)) + .FirstOrDefault(x => x.p.Name == argName); + + if (param?.i < attr.ConstructorArguments.Length && attr.ConstructorArguments[param.Value.i].Value is string value) + { + return value; + } + + return null; + } +} diff --git a/src/KubeOps.Generator/SyntaxReceiver/EntityFinalizerSyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/EntityFinalizerSyntaxReceiver.cs deleted file mode 100644 index d2dd5efff..000000000 --- a/src/KubeOps.Generator/SyntaxReceiver/EntityFinalizerSyntaxReceiver.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.SyntaxReceiver; - -internal sealed class EntityFinalizerSyntaxReceiver : ISyntaxContextReceiver -{ - private const string IEntityFinalizerMetadataName = "KubeOps.Abstractions.Reconciliation.Finalizer.IEntityFinalizer`1"; - -#pragma warning disable RS1024 - private readonly HashSet _visitedTypeSymbols = new(SymbolEqualityComparer.Default); -#pragma warning restore RS1024 - - public List<(ClassDeclarationSyntax Finalizer, string FullyQualifiedEntityName)> Finalizer { get; } = []; - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax) - { - return; - } - - if (context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol) - { - return; - } - - if (!_visitedTypeSymbols.Add(classSymbol)) - { - return; - } - - if (classSymbol.IsAbstract) - { - return; - } - - var iEntityFinalizerInterface = context.SemanticModel.Compilation.GetTypeByMetadataName(IEntityFinalizerMetadataName); - if (iEntityFinalizerInterface is null) - { - return; - } - - var implementedEntityFinalizerInterface = classSymbol.AllInterfaces - .FirstOrDefault(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iEntityFinalizerInterface)); - - var entityTypeSymbol = implementedEntityFinalizerInterface?.TypeArguments.FirstOrDefault(); - - if (entityTypeSymbol == null) - { - return; - } - - Finalizer.Add((classDeclarationSyntax, entityTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); - } -} diff --git a/src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs b/src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs new file mode 100644 index 000000000..5bdd82f0a --- /dev/null +++ b/src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace KubeOps.Generator.SyntaxReceiver; + +/// +/// An immutable array wrapper that implements structural (value-based) equality. +/// +/// +/// +/// Incremental generator pipeline models must compare by value so that equality can drive result +/// caching. compares by the underlying array reference, which would +/// defeat caching; this wrapper compares element by element instead. Roslyn ships an equivalent type +/// internally but does not expose it publicly, so it has to be re-declared here. This is a known, +/// repeatedly requested gap, see +/// dotnet/runtime#77183 (make +/// itself value-equatable), +/// dotnet/runtime#89318 (ship an +/// equatable collection type for incremental generators) and +/// dotnet/roslyn-analyzers#6352 +/// (which notes that nearly all generator authors have to hand-roll this type). +/// +/// +/// Unlike Roslyn's internal version this type deliberately omits the where T : IEquatable<T> +/// constraint and compares elements via . The constraint +/// would reject enum element types (an enum does not implement ), and we +/// need EquatableArray<SyntaxKind> for the cached entity modifiers. +/// +/// +/// The type of the elements stored in the array. +internal readonly struct EquatableArray(ImmutableArray array) : IEquatable>, IEnumerable +{ + public static readonly EquatableArray Empty = new(ImmutableArray.Empty); + + private readonly ImmutableArray _array = array; + + public int Count => _array.IsDefault ? 0 : _array.Length; + + public T this[int index] => _array[index]; + + public bool Equals(EquatableArray other) + { + if (_array.IsDefault) + { + return other._array.IsDefault; + } + + if (other._array.IsDefault || _array.Length != other._array.Length) + { + return false; + } + + for (var i = 0; i < _array.Length; i++) + { + if (!EqualityComparer.Default.Equals(_array[i], other._array[i])) + { + return false; + } + } + + return true; + } + + public override bool Equals(object? obj) => obj is EquatableArray other && Equals(other); + + public override int GetHashCode() + { + if (_array.IsDefault) + { + return 0; + } + + var hash = 17; + foreach (var item in _array) + { + hash = (hash * 31) + (item?.GetHashCode() ?? 0); + } + + return hash; + } + + public IEnumerator GetEnumerator() + => ((IEnumerable)(_array.IsDefault ? ImmutableArray.Empty : _array)).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs deleted file mode 100644 index 4e9a69e45..000000000 --- a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace KubeOps.Generator.SyntaxReceiver; - -internal sealed class KubernetesEntitySyntaxReceiver : ISyntaxContextReceiver -{ - private const string KindName = "Kind"; - private const string GroupName = "Group"; - private const string PluralName = "Plural"; - private const string VersionName = "ApiVersion"; - private const string DefaultVersion = "v1"; - - public List Entities { get; } = []; - -#pragma warning disable RS1024 - private HashSet DiscoveredEntities { get; } = new(SymbolEqualityComparer.Default); -#pragma warning restore RS1024 - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - // 1. entities from assembly (attributed) - if (context.Node is ClassDeclarationSyntax { AttributeLists.Count: > 0 } cls) - { - DiscoverEntitiesFromAssembly(context, cls); - } - - // 2. referenced entities from controllers and finalizers - if (context.Node is ClassDeclarationSyntax classDecl) - { - DiscoverEntitiesFromReferencedAssembliesByUsage(context, classDecl); - } - } - - private static bool IsEntityController(INamedTypeSymbol type) - => type.Name is "IEntityController"; - - private static bool IsEntityFinalizer(INamedTypeSymbol type) - => type.Name is "IEntityFinalizer"; - - private static string? GetAttributeValue(AttributeData attr, string argName) - { - var namedArg = attr.NamedArguments.FirstOrDefault(a => a.Key == argName); - if (namedArg.Value.Value is string s) - { - return s; - } - - var param = attr.AttributeConstructor?.Parameters - .Select((p, i) => (p, i)) - .FirstOrDefault(x => x.p.Name == argName); - - if (param?.i < attr.ConstructorArguments.Length && attr.ConstructorArguments[param.Value.i].Value is string value) - { - return value; - } - - return null; - } - - private static string? GetArgumentValue(SemanticModel model, AttributeSyntax attr, string argName) - { - if (attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName) - is not { Expression: { } expr }) - { - return null; - } - - if (model.GetConstantValue(expr) is { HasValue: true, Value: string s }) - { - return s; - } - - return expr is LiteralExpressionSyntax { Token.ValueText: { } value } - ? value - : null; - } - - private void DiscoverEntitiesFromAssembly(GeneratorSyntaxContext context, ClassDeclarationSyntax cls) - { - var attr = cls.AttributeLists - .SelectMany(a => a.Attributes) - .FirstOrDefault(a => a.Name.ToString() == "KubernetesEntity"); - - if (attr is null) - { - return; - } - - var typeSymbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, cls); - if (typeSymbol is not INamedTypeSymbol namedTypeSymbol) - { - return; - } - - var fullyQualifiedName = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - - // if the entity was already discovered via another controller/finalizer, - // remove it to register the entity as coming from the same assembly - if (!DiscoveredEntities.Add(namedTypeSymbol)) - { - var entity = Entities - .First(e => e.ClassDeclaration.FullyQualifiedName == fullyQualifiedName); - Entities.Remove(entity); - } - - Entities.Add( - new( - new( - ClassName: cls.Identifier.ToString(), - FullyQualifiedName: fullyQualifiedName, - Namespace: namedTypeSymbol.ContainingNamespace.IsGlobalNamespace - ? null - : namedTypeSymbol.ContainingNamespace.ToDisplayString(), - Modifiers: cls.Modifiers, - IsPartial: cls.Modifiers.Any(SyntaxKind.PartialKeyword), - HasParameterlessConstructor: cls.Members.Any(m - => m is ConstructorDeclarationSyntax { ParameterList.Parameters.Count: 0 }), - IsFromReferencedAssembly: false), - Kind: GetArgumentValue(context.SemanticModel, attr, KindName) ?? cls.Identifier.ToString(), - Version: GetArgumentValue(context.SemanticModel, attr, VersionName) ?? DefaultVersion, - Group: GetArgumentValue(context.SemanticModel, attr, GroupName), - Plural: GetArgumentValue(context.SemanticModel, attr, PluralName))); - } - - private void DiscoverEntitiesFromReferencedAssembliesByUsage(GeneratorSyntaxContext context, ClassDeclarationSyntax classDecl) - { - var symbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, classDecl); - if (symbol is not INamedTypeSymbol namedTypeSymbol) - { - return; - } - - foreach (var @interface in namedTypeSymbol.AllInterfaces.Where(i => - (IsEntityController(i) || IsEntityFinalizer(i)) - && i is { IsGenericType: true, TypeArguments.Length: > 0 })) - { - if (@interface.TypeArguments[0] is not INamedTypeSymbol entityType || !DiscoveredEntities.Add(entityType)) - { - continue; - } - - AddEntityFromSymbol(entityType); - } - } - - private void AddEntityFromSymbol(INamedTypeSymbol namedTypeSymbol) - { - var attr = namedTypeSymbol - .GetAttributes() - .FirstOrDefault(a => a.AttributeClass?.Name == "KubernetesEntityAttribute"); - - if (attr is null) - { - return; - } - - var kind = GetAttributeValue(attr, KindName) ?? namedTypeSymbol.Name; - var version = GetAttributeValue(attr, VersionName) ?? DefaultVersion; - var group = GetAttributeValue(attr, GroupName); - var plural = GetAttributeValue(attr, PluralName); - - Entities.Add( - new( - new( - ClassName: namedTypeSymbol.Name, - FullyQualifiedName: namedTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - Namespace: namedTypeSymbol.ContainingNamespace.IsGlobalNamespace - ? null - : namedTypeSymbol.ContainingNamespace.ToDisplayString(), - Modifiers: null, - IsPartial: false, - HasParameterlessConstructor: namedTypeSymbol.Constructors - .Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public), - IsFromReferencedAssembly: true), - Kind: kind, - Version: version, - Group: group, - Plural: plural)); - } -} From 9c47a713d154d1aac8079930df7025550d5e249a Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Fri, 12 Jun 2026 12:14:12 +0200 Subject: [PATCH 2/4] chore: simplify namespaces and remove redundant usings across the codebase --- examples/ConversionWebhookOperator/Program.cs | 4 ---- examples/Operator/Program.cs | 1 + examples/WebhookOperator/Program.cs | 12 ++++++++++++ .../KubeOpsServiceDefaultsExtensions.cs | 1 - src/KubeOps.Cli/Commands/Generator/Generate.cs | 1 - src/KubeOps.Cli/Commands/Management/Install.cs | 1 - src/KubeOps.Cli/Commands/Management/Uninstall.cs | 1 - .../AttributedEntity.cs | 2 +- .../ClassDeclarationMetaData.cs | 2 +- .../Discovery/ControllerRegistration.cs | 9 +++++++++ .../EntityDiscovery.cs | 11 +---------- .../{SyntaxReceiver => Discovery}/EquatableArray.cs | 13 ++----------- .../Discovery/FinalizerRegistration.cs | 10 ++++++++++ .../Generators/ControllerRegistrationGenerator.cs | 2 +- .../Generators/EntityDefinitionGenerator.cs | 2 +- .../Generators/EntityInitializerGenerator.cs | 2 +- .../Generators/FinalizerRegistrationGenerator.cs | 2 +- .../LocalTunnel/TunnelWebhookService.cs | 3 +-- .../Webhooks/Conversion/ConversionResponse.cs | 2 -- src/KubeOps.Operator/Events/EventNameEncoder.cs | 1 - src/KubeOps.Operator/Logging/EntityLoggingScope.cs | 1 - .../KubeOpsHosting.Test.cs | 2 +- .../KubeOpsServiceDefaults.Test.cs | 4 +--- .../Management/Install.Integration.Test.cs | 3 +-- .../Events/EventPublisher.Integration.Test.cs | 3 --- .../EventPublisherCustomResourceFactory.Test.cs | 2 +- 26 files changed, 46 insertions(+), 51 deletions(-) rename src/KubeOps.Generator/{SyntaxReceiver => Discovery}/AttributedEntity.cs (89%) rename src/KubeOps.Generator/{SyntaxReceiver => Discovery}/ClassDeclarationMetaData.cs (92%) create mode 100644 src/KubeOps.Generator/Discovery/ControllerRegistration.cs rename src/KubeOps.Generator/{SyntaxReceiver => Discovery}/EntityDiscovery.cs (97%) rename src/KubeOps.Generator/{SyntaxReceiver => Discovery}/EquatableArray.cs (91%) create mode 100644 src/KubeOps.Generator/Discovery/FinalizerRegistration.cs diff --git a/examples/ConversionWebhookOperator/Program.cs b/examples/ConversionWebhookOperator/Program.cs index d5f4110a7..3a343b874 100644 --- a/examples/ConversionWebhookOperator/Program.cs +++ b/examples/ConversionWebhookOperator/Program.cs @@ -2,11 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using System.Net; - using KubeOps.Operator; -using KubeOps.Operator.Web.Builder; -using KubeOps.Operator.Web.Certificates; var builder = WebApplication.CreateBuilder(args); var opBuilder = builder.Services diff --git a/examples/Operator/Program.cs b/examples/Operator/Program.cs index 8615a6a55..96c16de92 100644 --- a/examples/Operator/Program.cs +++ b/examples/Operator/Program.cs @@ -5,6 +5,7 @@ #if DEBUG using KubeOps.Abstractions.Crds; #endif + using KubeOps.Operator; using Microsoft.Extensions.Hosting; diff --git a/examples/WebhookOperator/Program.cs b/examples/WebhookOperator/Program.cs index b166e6668..c42bdf351 100644 --- a/examples/WebhookOperator/Program.cs +++ b/examples/WebhookOperator/Program.cs @@ -2,16 +2,28 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +#if DEBUG using System.Net; +#endif using KubeOps.Operator; + +#if DEBUG using KubeOps.Operator.Web.Builder; using KubeOps.Operator.Web.Certificates; +#endif var builder = WebApplication.CreateBuilder(args); + +#if !DEBUG +builder.Services + .AddKubernetesOperator() + .RegisterComponents(); +#else var opBuilder = builder.Services .AddKubernetesOperator() .RegisterComponents(); +#endif #if DEBUG const string ip = "192.168.1.100"; diff --git a/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs b/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs index d69d6739f..839bc4dd9 100644 --- a/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs +++ b/src/KubeOps.Aspire/KubeOpsServiceDefaultsExtensions.cs @@ -4,7 +4,6 @@ using KubeOps.Abstractions.Builder; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; diff --git a/src/KubeOps.Cli/Commands/Generator/Generate.cs b/src/KubeOps.Cli/Commands/Generator/Generate.cs index 743db12bb..d70f07b41 100644 --- a/src/KubeOps.Cli/Commands/Generator/Generate.cs +++ b/src/KubeOps.Cli/Commands/Generator/Generate.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.CommandLine; -using System.CommandLine.Help; namespace KubeOps.Cli.Commands.Generator; diff --git a/src/KubeOps.Cli/Commands/Management/Install.cs b/src/KubeOps.Cli/Commands/Management/Install.cs index bd6d7c53c..b5c8b75eb 100644 --- a/src/KubeOps.Cli/Commands/Management/Install.cs +++ b/src/KubeOps.Cli/Commands/Management/Install.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.CommandLine; -using System.CommandLine.Invocation; using k8s; using k8s.Autorest; diff --git a/src/KubeOps.Cli/Commands/Management/Uninstall.cs b/src/KubeOps.Cli/Commands/Management/Uninstall.cs index 0615dbcda..3b33135c2 100644 --- a/src/KubeOps.Cli/Commands/Management/Uninstall.cs +++ b/src/KubeOps.Cli/Commands/Management/Uninstall.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.CommandLine; -using System.CommandLine.Invocation; using k8s; using k8s.Autorest; diff --git a/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs b/src/KubeOps.Generator/Discovery/AttributedEntity.cs similarity index 89% rename from src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs rename to src/KubeOps.Generator/Discovery/AttributedEntity.cs index 3ef95b34b..76589e98a 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/AttributedEntity.cs +++ b/src/KubeOps.Generator/Discovery/AttributedEntity.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -namespace KubeOps.Generator.SyntaxReceiver; +namespace KubeOps.Generator.Discovery; internal record struct AttributedEntity( ClassDeclarationMetaData ClassDeclaration, diff --git a/src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs b/src/KubeOps.Generator/Discovery/ClassDeclarationMetaData.cs similarity index 92% rename from src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs rename to src/KubeOps.Generator/Discovery/ClassDeclarationMetaData.cs index 781b3cb10..106281b90 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/ClassDeclarationMetaData.cs +++ b/src/KubeOps.Generator/Discovery/ClassDeclarationMetaData.cs @@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis.CSharp; -namespace KubeOps.Generator.SyntaxReceiver; +namespace KubeOps.Generator.Discovery; internal record struct ClassDeclarationMetaData( string ClassName, diff --git a/src/KubeOps.Generator/Discovery/ControllerRegistration.cs b/src/KubeOps.Generator/Discovery/ControllerRegistration.cs new file mode 100644 index 000000000..ad968d5ac --- /dev/null +++ b/src/KubeOps.Generator/Discovery/ControllerRegistration.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +namespace KubeOps.Generator.Discovery; + +internal record struct ControllerRegistration( + string FullyQualifiedController, + string FullyQualifiedEntityName); diff --git a/src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs b/src/KubeOps.Generator/Discovery/EntityDiscovery.cs similarity index 97% rename from src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs rename to src/KubeOps.Generator/Discovery/EntityDiscovery.cs index e4a1ac4de..bf4d176cb 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/EntityDiscovery.cs +++ b/src/KubeOps.Generator/Discovery/EntityDiscovery.cs @@ -8,16 +8,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -namespace KubeOps.Generator.SyntaxReceiver; - -internal record struct ControllerRegistration( - string FullyQualifiedController, - string FullyQualifiedEntityName); - -internal record struct FinalizerRegistration( - string FullyQualifiedFinalizer, - string IdentifierName, - string FullyQualifiedEntityName); +namespace KubeOps.Generator.Discovery; internal static class EntityDiscovery { diff --git a/src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs b/src/KubeOps.Generator/Discovery/EquatableArray.cs similarity index 91% rename from src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs rename to src/KubeOps.Generator/Discovery/EquatableArray.cs index 5bdd82f0a..4f6453bb5 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/EquatableArray.cs +++ b/src/KubeOps.Generator/Discovery/EquatableArray.cs @@ -3,10 +3,9 @@ // See the LICENSE file in the project root for more information. using System.Collections; -using System.Collections.Generic; using System.Collections.Immutable; -namespace KubeOps.Generator.SyntaxReceiver; +namespace KubeOps.Generator.Discovery; /// /// An immutable array wrapper that implements structural (value-based) equality. @@ -55,15 +54,7 @@ public bool Equals(EquatableArray other) return false; } - for (var i = 0; i < _array.Length; i++) - { - if (!EqualityComparer.Default.Equals(_array[i], other._array[i])) - { - return false; - } - } - - return true; + return !_array.Where((t, i) => !EqualityComparer.Default.Equals(t, other._array[i])).Any(); } public override bool Equals(object? obj) => obj is EquatableArray other && Equals(other); diff --git a/src/KubeOps.Generator/Discovery/FinalizerRegistration.cs b/src/KubeOps.Generator/Discovery/FinalizerRegistration.cs new file mode 100644 index 000000000..82acd2afc --- /dev/null +++ b/src/KubeOps.Generator/Discovery/FinalizerRegistration.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +namespace KubeOps.Generator.Discovery; + +internal record struct FinalizerRegistration( + string FullyQualifiedFinalizer, + string IdentifierName, + string FullyQualifiedEntityName); diff --git a/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs b/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs index 7bf467a29..fe0ce063a 100644 --- a/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs +++ b/src/KubeOps.Generator/Generators/ControllerRegistrationGenerator.cs @@ -4,7 +4,7 @@ using System.Text; -using KubeOps.Generator.SyntaxReceiver; +using KubeOps.Generator.Discovery; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs b/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs index af6767914..1114acabc 100644 --- a/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs +++ b/src/KubeOps.Generator/Generators/EntityDefinitionGenerator.cs @@ -4,7 +4,7 @@ using System.Text; -using KubeOps.Generator.SyntaxReceiver; +using KubeOps.Generator.Discovery; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs b/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs index 87ca1e10a..d0aa89a60 100644 --- a/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs +++ b/src/KubeOps.Generator/Generators/EntityInitializerGenerator.cs @@ -4,7 +4,7 @@ using System.Text; -using KubeOps.Generator.SyntaxReceiver; +using KubeOps.Generator.Discovery; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs b/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs index 9893adfbe..284e834ec 100644 --- a/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs +++ b/src/KubeOps.Generator/Generators/FinalizerRegistrationGenerator.cs @@ -4,7 +4,7 @@ using System.Text; -using KubeOps.Generator.SyntaxReceiver; +using KubeOps.Generator.Discovery; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs b/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs index 59d2db854..c722f6ad7 100644 --- a/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs +++ b/src/KubeOps.Operator.Web/LocalTunnel/TunnelWebhookService.cs @@ -4,7 +4,6 @@ using KubeOps.Abstractions.Webhooks; using KubeOps.KubernetesClient; -using KubeOps.Operator.Web.Certificates; using KubeOps.Operator.Web.Webhooks; using Microsoft.Extensions.Hosting; @@ -12,7 +11,7 @@ namespace KubeOps.Operator.Web.LocalTunnel; -internal class TunnelWebhookService( +internal sealed class TunnelWebhookService( ILogger logger, IKubernetesClient client, WebhookLoader loader, diff --git a/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionResponse.cs b/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionResponse.cs index 6ce5c00c6..e4cdfeb45 100644 --- a/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionResponse.cs +++ b/src/KubeOps.Operator.Web/Webhooks/Conversion/ConversionResponse.cs @@ -7,8 +7,6 @@ using k8s; -using KubeOps.Operator.Web.Webhooks.Admission; - using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/src/KubeOps.Operator/Events/EventNameEncoder.cs b/src/KubeOps.Operator/Events/EventNameEncoder.cs index b0bfcc8b0..0c0e450d8 100644 --- a/src/KubeOps.Operator/Events/EventNameEncoder.cs +++ b/src/KubeOps.Operator/Events/EventNameEncoder.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Security.Cryptography; - using System.Text; namespace KubeOps.Operator.Events; diff --git a/src/KubeOps.Operator/Logging/EntityLoggingScope.cs b/src/KubeOps.Operator/Logging/EntityLoggingScope.cs index 7bccad2d5..c82169ed0 100644 --- a/src/KubeOps.Operator/Logging/EntityLoggingScope.cs +++ b/src/KubeOps.Operator/Logging/EntityLoggingScope.cs @@ -8,7 +8,6 @@ using k8s.Models; using KubeOps.Abstractions.Reconciliation; -using KubeOps.Abstractions.Reconciliation.Queue; namespace KubeOps.Operator.Logging; diff --git a/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs b/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs index 9646b75bc..6365b9ec8 100644 --- a/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs +++ b/test/KubeOps.Aspire.Hosting.Test/KubeOpsHosting.Test.cs @@ -125,7 +125,7 @@ public void PublishAsKubernetesOperator_Binds_To_Kubernetes_Environment() } private static IDistributedApplicationTestingBuilder CreateBuilder() - => global::Aspire.Hosting.Testing.DistributedApplicationTestingBuilder + => DistributedApplicationTestingBuilder .CreateAsync() .GetAwaiter() .GetResult(); diff --git a/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs b/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs index e173bc739..931839497 100644 --- a/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs +++ b/test/KubeOps.Aspire.Test/KubeOpsServiceDefaults.Test.cs @@ -4,8 +4,6 @@ using FluentAssertions; -using KubeOps.Aspire; - using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; @@ -16,7 +14,7 @@ namespace KubeOps.Aspire.Test; -public class KubeOpsServiceDefaultsTest +public sealed class KubeOpsServiceDefaultsTest { [Fact] public void Should_Register_OpenTelemetry_Providers() diff --git a/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs b/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs index 62512a1b0..249c99a58 100644 --- a/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs +++ b/test/KubeOps.Cli.Test/Management/Install.Integration.Test.cs @@ -41,7 +41,6 @@ public async Task Should_Install_Crds_In_Cluster() public async Task Should_Generate_Valid_Installers_In_Cluster() { var console = new TestConsole(); - var client = new Kubernetes(KubernetesClientConfiguration.BuildDefaultConfig()); var cmd = OperatorGenerator.Command; var result = cmd.Parse(["operator", "test", ProjectPath]); @@ -55,7 +54,7 @@ public async Task Should_Generate_Valid_Installers_In_Cluster() if (consoleLine.Equals(separator)) { groups.Add(sb.ToString()); - sb = new StringBuilder(); + sb = new(); } else { diff --git a/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs b/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs index 25f9ef319..c6998b6e7 100644 --- a/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs +++ b/test/KubeOps.Operator.Test/Events/EventPublisher.Integration.Test.cs @@ -2,9 +2,6 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using System.Security.Cryptography; -using System.Text; - using FluentAssertions; using k8s.Models; diff --git a/test/KubeOps.Operator.Test/Events/EventPublisherCustomResourceFactory.Test.cs b/test/KubeOps.Operator.Test/Events/EventPublisherCustomResourceFactory.Test.cs index 003253cdb..2b429ed6e 100644 --- a/test/KubeOps.Operator.Test/Events/EventPublisherCustomResourceFactory.Test.cs +++ b/test/KubeOps.Operator.Test/Events/EventPublisherCustomResourceFactory.Test.cs @@ -67,7 +67,7 @@ public async Task Should_Publish_Event_Created_By_Custom_EventResourceFactory() // Register custom factory and mock client BEFORE AddKubernetesOperator. services.AddSingleton(mockFactory.Object); services.AddSingleton(mockClient.Object); - services.AddSingleton(new Mock().Object); + services.AddSingleton(new Mock().Object); services.AddLogging(); services.AddKubernetesOperator(); From 51431e926a572179b038e8b25470de84f6fc4e78 Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Fri, 12 Jun 2026 12:20:58 +0200 Subject: [PATCH 3/4] chore: removed more warnings --- examples/ConversionWebhookOperator/Program.cs | 16 ++++++++++++++++ .../Webhooks/TestConversionWebhook.cs | 4 ++-- .../Entities/IEntityFieldSelector{TEntity}.cs | 3 --- .../Entities/IEntityLabelSelector{TEntity}.cs | 3 --- .../KubeOpsHostingExtensions.cs | 4 ++-- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/examples/ConversionWebhookOperator/Program.cs b/examples/ConversionWebhookOperator/Program.cs index 3a343b874..16a17cd94 100644 --- a/examples/ConversionWebhookOperator/Program.cs +++ b/examples/ConversionWebhookOperator/Program.cs @@ -2,12 +2,28 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +#if DEBUG +using System.Net; +#endif + using KubeOps.Operator; +#if DEBUG +using KubeOps.Operator.Web.Builder; +using KubeOps.Operator.Web.Certificates; +#endif + var builder = WebApplication.CreateBuilder(args); + +#if !DEBUG +builder.Services + .AddKubernetesOperator() + .RegisterComponents(); +#else var opBuilder = builder.Services .AddKubernetesOperator() .RegisterComponents(); +#endif #if DEBUG const string ip = "192.168.1.100"; diff --git a/examples/ConversionWebhookOperator/Webhooks/TestConversionWebhook.cs b/examples/ConversionWebhookOperator/Webhooks/TestConversionWebhook.cs index 2a2aa3abc..0945cb3be 100644 --- a/examples/ConversionWebhookOperator/Webhooks/TestConversionWebhook.cs +++ b/examples/ConversionWebhookOperator/Webhooks/TestConversionWebhook.cs @@ -19,7 +19,7 @@ public sealed class TestConversionWebhook : ConversionWebhook new V1ToV3(), new V2ToV3(), }; - private class V1ToV3 : IEntityConverter + private sealed class V1ToV3 : IEntityConverter { public V3TestEntity Convert(V1TestEntity from) { @@ -38,7 +38,7 @@ public V1TestEntity Revert(V3TestEntity to) } } - private class V2ToV3 : IEntityConverter + private sealed class V2ToV3 : IEntityConverter { public V3TestEntity Convert(V2TestEntity from) { diff --git a/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs b/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs index fcc2251b7..a5a2f70c4 100644 --- a/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs +++ b/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs @@ -7,9 +7,6 @@ namespace KubeOps.Abstractions.Entities; -// This is the same pattern used by Microsoft on ILogger. -// An alternative would be to use a KeyedSingleton when registering this however that's only valid from .NET 8 and above. -// Other methods are far less elegant #pragma warning disable S2326 public interface IEntityFieldSelector where TEntity : IKubernetesObject diff --git a/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs b/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs index d091b5699..b72af82a9 100644 --- a/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs +++ b/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs @@ -7,9 +7,6 @@ namespace KubeOps.Abstractions.Entities; -// This is the same pattern used by Microsoft on ILogger. -// An alternative would be to use a KeyedSingleton when registering this however that's only valid from .NET 8 and above. -// Other methods are far less elegant #pragma warning disable S2326 public interface IEntityLabelSelector where TEntity : IKubernetesObject diff --git a/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs b/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs index bd77d502e..6ec717bb3 100644 --- a/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs +++ b/src/KubeOps.Aspire.Hosting/KubeOpsHostingExtensions.cs @@ -377,7 +377,7 @@ private static void ConfigureOperatorPod( string name, KubeOpsKubernetesManifestOptions options) { - var podSpec = kubernetes.Workload?.PodTemplate?.Spec; + var podSpec = kubernetes.Workload?.PodTemplate.Spec; if (podSpec is null) { return; @@ -626,7 +626,7 @@ private static void MergeKubeOpsDeployment(KubernetesResource kubernetes, JsonOb } var generatedPodSpec = generatedSpec["template"]?["spec"] as JsonObject; - var podSpec = kubernetes.Workload?.PodTemplate?.Spec; + var podSpec = kubernetes.Workload?.PodTemplate.Spec; if (generatedPodSpec is null || podSpec is null) { return; From 562e4c3b2ff09670f5ca329d7c46de6583e6c53a Mon Sep 17 00:00:00 2001 From: Marcus Kimpenhaus Date: Fri, 12 Jun 2026 16:43:49 +0200 Subject: [PATCH 4/4] chore: update plural name handling and dependencies - Renamed constant `Plural` to `PluralName` in entity discovery. - Adjusted tests to include custom plural name scenarios. - Updated `Microsoft.CodeAnalysis.CSharp` to version 4.8.0 with comments explaining compatibility constraints. - Modified `renovate.json` to improve `KubeOps.Generator.csproj` matching logic. --- renovate.json | 4 ++-- .../Entities/IEntityFieldSelector{TEntity}.cs | 3 +++ .../Entities/IEntityLabelSelector{TEntity}.cs | 3 +++ .../Discovery/EntityDiscovery.cs | 10 ++++----- .../Discovery/EquatableArray.cs | 10 ++++++++- .../KubeOps.Generator.csproj | 9 +++++++- ...nitionGenerator.ReferencedAssembly.Test.cs | 6 ++--- .../EntityDefinitionGenerator.Test.cs | 22 +++++++++++++++++++ 8 files changed, 55 insertions(+), 12 deletions(-) diff --git a/renovate.json b/renovate.json index 2ddf9ba85..82413cdb6 100644 --- a/renovate.json +++ b/renovate.json @@ -17,9 +17,9 @@ "enabled": false }, { - "matchManagers": ["dotnet"], + "matchManagers": ["nuget"], "matchDepNames": ["Microsoft.CodeAnalysis.CSharp"], - "matchFileNames": ["KubeOps.Generator.csproj"], + "matchFileNames": ["**/KubeOps.Generator.csproj"], "enabled": false }, { diff --git a/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs b/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs index a5a2f70c4..ba0a16de4 100644 --- a/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs +++ b/src/KubeOps.Abstractions/Entities/IEntityFieldSelector{TEntity}.cs @@ -7,6 +7,9 @@ namespace KubeOps.Abstractions.Entities; +// This is the same pattern used by Microsoft on ILogger. +// An alternative would be to use a KeyedSingleton when registering this; however, that's only valid from .NET 8 and above. +// Other methods are far less elegant #pragma warning disable S2326 public interface IEntityFieldSelector where TEntity : IKubernetesObject diff --git a/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs b/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs index b72af82a9..0bb3a47f6 100644 --- a/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs +++ b/src/KubeOps.Abstractions/Entities/IEntityLabelSelector{TEntity}.cs @@ -7,6 +7,9 @@ namespace KubeOps.Abstractions.Entities; +// This is the same pattern used by Microsoft on ILogger. +// An alternative would be to use a KeyedSingleton when registering this; however, that's only valid from .NET 8 and above. +// Other methods are far less elegant #pragma warning disable S2326 public interface IEntityLabelSelector where TEntity : IKubernetesObject diff --git a/src/KubeOps.Generator/Discovery/EntityDiscovery.cs b/src/KubeOps.Generator/Discovery/EntityDiscovery.cs index bf4d176cb..710e88188 100644 --- a/src/KubeOps.Generator/Discovery/EntityDiscovery.cs +++ b/src/KubeOps.Generator/Discovery/EntityDiscovery.cs @@ -19,7 +19,7 @@ internal static class EntityDiscovery private const string KindName = "Kind"; private const string GroupName = "Group"; - private const string PluralName = "Plural"; + private const string PluralName = "PluralName"; private const string VersionName = "ApiVersion"; private const string DefaultVersion = "v1"; @@ -82,7 +82,7 @@ private static EquatableArray Merge( .OrderBy(e => e.ClassDeclaration.FullyQualifiedName, StringComparer.Ordinal) .ToImmutableArray(); - return new EquatableArray(ordered); + return new(ordered); } private static AttributedEntity? GetLocalEntity(GeneratorSyntaxContext context) @@ -105,13 +105,13 @@ private static EquatableArray Merge( } return new AttributedEntity( - new ClassDeclarationMetaData( + new( ClassName: cls.Identifier.Text, FullyQualifiedName: symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace: symbol.ContainingNamespace.IsGlobalNamespace ? null : symbol.ContainingNamespace.ToDisplayString(), - ModifierKinds: new EquatableArray( + ModifierKinds: new( cls.Modifiers.Select(m => m.Kind()).ToImmutableArray()), IsPartial: cls.Modifiers.Any(SyntaxKind.PartialKeyword), HasParameterlessConstructor: cls.Members.Any(m @@ -181,7 +181,7 @@ private static ImmutableArray GetReferencedEntities(GeneratorS } return new AttributedEntity( - new ClassDeclarationMetaData( + new( ClassName: symbol.Name, FullyQualifiedName: symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace: symbol.ContainingNamespace.IsGlobalNamespace diff --git a/src/KubeOps.Generator/Discovery/EquatableArray.cs b/src/KubeOps.Generator/Discovery/EquatableArray.cs index 4f6453bb5..d7ee59706 100644 --- a/src/KubeOps.Generator/Discovery/EquatableArray.cs +++ b/src/KubeOps.Generator/Discovery/EquatableArray.cs @@ -54,7 +54,15 @@ public bool Equals(EquatableArray other) return false; } - return !_array.Where((t, i) => !EqualityComparer.Default.Equals(t, other._array[i])).Any(); + for (var i = 0; i < _array.Length; i++) + { + if (!EqualityComparer.Default.Equals(_array[i], other._array[i])) + { + return false; + } + } + + return true; } public override bool Equals(object? obj) => obj is EquatableArray other && Equals(other); diff --git a/src/KubeOps.Generator/KubeOps.Generator.csproj b/src/KubeOps.Generator/KubeOps.Generator.csproj index cf9ef7828..2ef8282ce 100644 --- a/src/KubeOps.Generator/KubeOps.Generator.csproj +++ b/src/KubeOps.Generator/KubeOps.Generator.csproj @@ -19,7 +19,14 @@ - + + diff --git a/test/KubeOps.Generator.Test/EntityDefinitionGenerator.ReferencedAssembly.Test.cs b/test/KubeOps.Generator.Test/EntityDefinitionGenerator.ReferencedAssembly.Test.cs index b5000e39f..e8b8f6fe0 100644 --- a/test/KubeOps.Generator.Test/EntityDefinitionGenerator.ReferencedAssembly.Test.cs +++ b/test/KubeOps.Generator.Test/EntityDefinitionGenerator.ReferencedAssembly.Test.cs @@ -40,7 +40,7 @@ public sealed class V1TestEntityController : IEntityController + { + } + """, + """ + // + // This code was generated by a tool. + // Changes to this file may cause incorrect behavior and will be lost if the code is regenerated. + // + #pragma warning disable CS1591 + using KubeOps.Abstractions.Builder; + using KubeOps.Abstractions.Entities; + + public static class EntityDefinitions + { + public static readonly EntityMetadata V1TestEntity = new("TestEntity", "v1", "testing.dev", "testentities"); + } + """, + TestDisplayName = "Test entity with custom plural name")] public void Should_Generate_Correct_Code(string input, string expectedResult) { var inputCompilation = input.CreateCompilation();