From e223df8520bcf6052a22f2b50d19d7c0e90f34d2 Mon Sep 17 00:00:00 2001 From: Shaun Becker Date: Mon, 3 Nov 2025 17:25:38 -0500 Subject: [PATCH 1/2] Update KubernetesEntitySyntaxReceiver to support reading entity metadata from const variables --- .../KubernetesEntitySyntaxReceiver.cs | 46 +++++++++++++++---- .../EntityInitializerGenerator.Test.cs | 38 +++++++++++++++ 2 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs index f98bcc0d..8509bb50 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs @@ -3,6 +3,7 @@ // 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; @@ -11,7 +12,7 @@ internal sealed class KubernetesEntitySyntaxReceiver : ISyntaxContextReceiver { 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"; @@ -28,15 +29,40 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) Entities.Add(new( cls, - GetArgumentValue(attr, KindName) ?? cls.Identifier.ToString(), - GetArgumentValue(attr, VersionName) ?? DefaultVersion, - GetArgumentValue(attr, GroupName), - GetArgumentValue(attr, PluralName))); + GetArgumentValue(cls, attr, KindName) ?? cls.Identifier.ToString(), + GetArgumentValue(cls, attr, VersionName) ?? DefaultVersion, + GetArgumentValue(cls, attr, GroupName), + GetArgumentValue(cls, attr, PluralName))); } - private static string? GetArgumentValue(AttributeSyntax attr, string argName) => - attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName) is - { Expression: LiteralExpressionSyntax { Token.ValueText: { } value } } - ? value - : null; + private static string? GetArgumentValue(ClassDeclarationSyntax cls, AttributeSyntax attr, string argName) + { + var argument = attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName); + if (argument != null) + { + if (argument.Expression is LiteralExpressionSyntax { Token.ValueText: { } value }) + { + return value; + } + + if (argument.Expression is IdentifierNameSyntax { Identifier.ValueText: { } ident }) + { + var field = cls.Members + .OfType() + .FirstOrDefault(f => f.Modifiers.Any(SyntaxKind.ConstKeyword) && + f.Declaration.Variables.Any(v => v.Identifier.ValueText == ident)); + if (field != null) + { + var variable = field.Declaration.Variables + .First(v => v.Identifier.ValueText == ident); + if (variable.Initializer?.Value is LiteralExpressionSyntax { Token.ValueText: { } fieldValue }) + { + return fieldValue; + } + } + } + } + + return null; + } } diff --git a/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs b/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs index 3e65a430..70e1118a 100644 --- a/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs +++ b/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs @@ -336,4 +336,42 @@ public V1TestEntity() .ToString().ReplaceLineEndings(); result.Should().Be(expectedResult); } + + [Fact] + public void Should_Generate_Default_Ctor_For_Partial_Entity_With_Metadata_Const() + { + var inputCompilation = """ + [KubernetesEntity(Group = KubeGroup, ApiVersion = KubeApiVersion, Kind = KubeKind, PluralName = KubePluralName)] + public partial class V1TestEntity : IKubernetesObject + { + public const string KubeApiVersion = "v1"; + public const string KubeGroup = "testing.dev"; + public const string KubeKind = "TestEntity"; + public const string KubePluralName = "testentities"; + } + """.CreateCompilation(); + var expectedResult = """ + // + // 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 + public partial class V1TestEntity + { + public V1TestEntity() + { + ApiVersion = "testing.dev/v1"; + Kind = "TestEntity"; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out ImmutableArray _); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } } From e54cccc2cff1cb14d0137436ceade0a60721fa2a Mon Sep 17 00:00:00 2001 From: Shaun Becker Date: Tue, 4 Nov 2025 11:49:59 -0500 Subject: [PATCH 2/2] Refactor to use GeneratorSyntaxContext.SemanticModel.GetConstantValue to eliminate the handling for different expression types --- .../KubernetesEntitySyntaxReceiver.cs | 35 +++++----------- .../EntityInitializerGenerator.Test.cs | 40 +++++++++++++++++++ 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs index 8509bb50..586565b9 100644 --- a/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs +++ b/src/KubeOps.Generator/SyntaxReceiver/KubernetesEntitySyntaxReceiver.cs @@ -3,7 +3,6 @@ // 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; @@ -29,37 +28,21 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) Entities.Add(new( cls, - GetArgumentValue(cls, attr, KindName) ?? cls.Identifier.ToString(), - GetArgumentValue(cls, attr, VersionName) ?? DefaultVersion, - GetArgumentValue(cls, attr, GroupName), - GetArgumentValue(cls, attr, PluralName))); + GetArgumentValue(context, attr, KindName) ?? cls.Identifier.ToString(), + GetArgumentValue(context, attr, VersionName) ?? DefaultVersion, + GetArgumentValue(context, attr, GroupName), + GetArgumentValue(context, attr, PluralName))); } - private static string? GetArgumentValue(ClassDeclarationSyntax cls, AttributeSyntax attr, string argName) + private static string? GetArgumentValue(GeneratorSyntaxContext context, AttributeSyntax attr, string argName) { - var argument = attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName); + var argument = attr.ArgumentList?.Arguments.FirstOrDefault(a => a.NameEquals?.Name.ToString() == argName)?.Expression; if (argument != null) { - if (argument.Expression is LiteralExpressionSyntax { Token.ValueText: { } value }) + var constValue = context.SemanticModel.GetConstantValue(argument); + if (constValue is { HasValue: true, Value: string attrValue }) { - return value; - } - - if (argument.Expression is IdentifierNameSyntax { Identifier.ValueText: { } ident }) - { - var field = cls.Members - .OfType() - .FirstOrDefault(f => f.Modifiers.Any(SyntaxKind.ConstKeyword) && - f.Declaration.Variables.Any(v => v.Identifier.ValueText == ident)); - if (field != null) - { - var variable = field.Declaration.Variables - .First(v => v.Identifier.ValueText == ident); - if (variable.Initializer?.Value is LiteralExpressionSyntax { Token.ValueText: { } fieldValue }) - { - return fieldValue; - } - } + return attrValue; } } diff --git a/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs b/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs index 70e1118a..34d319dc 100644 --- a/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs +++ b/test/KubeOps.Generator.Test/EntityInitializerGenerator.Test.cs @@ -374,4 +374,44 @@ public V1TestEntity() .ToString().ReplaceLineEndings(); result.Should().Be(expectedResult); } + + [Fact] + public void Should_Generate_Default_Ctor_For_Partial_Entity_With_External_Metadata_Const() + { + var inputCompilation = """ + internal static class Constants + { + public const string KubeApiVersion = "v1"; + public const string KubeGroup = "testing.dev"; + public const string KubeKind = "TestEntity"; + public const string KubePluralName = "testentities"; + } + + [KubernetesEntity(Group = Constants.KubeGroup, ApiVersion = Constants.KubeApiVersion, Kind = Constants.KubeKind, PluralName = Constants.KubePluralName)] + public partial class V1TestEntity : IKubernetesObject; + """.CreateCompilation(); + var expectedResult = """ + // + // 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 + public partial class V1TestEntity + { + public V1TestEntity() + { + ApiVersion = "testing.dev/v1"; + Kind = "TestEntity"; + } + } + """.ReplaceLineEndings(); + + var driver = CSharpGeneratorDriver.Create(new EntityInitializerGenerator()); + driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var output, out ImmutableArray _); + + var result = output.SyntaxTrees + .First(s => s.FilePath.Contains("V1TestEntity.init.g.cs")) + .ToString().ReplaceLineEndings(); + result.Should().Be(expectedResult); + } }