Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions NuGet.Config
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<packageSourceMapping>
<!-- Map all packages to nuget.org by default -->
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
81 changes: 75 additions & 6 deletions src/AltaSoft.Choice.Generator/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,16 @@ internal static void Execute(in ImmutableArray<INamedTypeSymbol?> typesToGenerat
DeclaredAccessibility: Accessibility.Public
}).ToList();

var sb = Process(typeSymbol, partialProperties);
// Get ordinary (non-partial) required properties
var ordinaryRequiredProperties = typeSymbol.GetMembersOfType<IPropertySymbol>().Where(x
=> x is
{
IsStatic: false, IsWriteOnly: false, CanBeReferencedByName: true, IsPartialDefinition: false,
DeclaredAccessibility: Accessibility.Public,
IsRequired: true
}).ToList();

var sb = Process(typeSymbol, partialProperties, ordinaryRequiredProperties);
context.AddSource($"{typeSymbol.Name}.g.cs", sb.ToString());
}

Expand All @@ -55,7 +64,7 @@ internal static void Execute(in ImmutableArray<INamedTypeSymbol?> typesToGenerat
}
}

private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IPropertySymbol> properties)
private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IPropertySymbol> properties, List<IPropertySymbol> ordinaryRequiredProperties)
{
var processedProperties = properties.ConvertAll(ProcessProperty);
var usingStatements = processedProperties.Select(x => x.Namespace).Concat(s_baseNamespaces);
Expand All @@ -67,6 +76,10 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp

sb.AppendLine("#pragma warning disable CS8774 // Member must have a non-null value when exiting.")
.AppendLine("#pragma warning disable CS0628 // New protected member declared in sealed type")
.AppendLine("#pragma warning disable CS0618 // Type or member is obsolete")
.AppendLine("#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor")
.AppendLine("#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member")
.AppendLine("#pragma warning disable IDE0290 // Use primary constructor")
.NewLine();

sb.AppendClass(typeSymbol.IsRecord, typeSymbol.GetModifiers() ?? "public partial", typeSymbol.Name);
Expand All @@ -81,6 +94,7 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp
{
sb.AppendLine("[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]");
}

sb.Append(typeSymbol.IsAbstract ? "protected " : "public ").Append(typeSymbol.Name).AppendLine("()")
.OpenBracket()
.CloseBracket()
Expand Down Expand Up @@ -176,10 +190,56 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp
{
var typeFullName = typeSymbol.GetFullName();
sb.AppendSummary($"Creates a new <see cref=\"{typeFullName}\"/> instance and sets its value using the specified {prop.TypeSymbol.GetCrefForType()}.");
sb.AppendParamDescription("value", "The value to assign to the created choice instance.");

sb.Append($"public static {typeFullName} CreateAs").Append(prop.Name).Append("(")
.Append(prop.TypeName).Append(" value) => new () { ").Append(prop.Name).AppendLine(" = value };");
var requiredParamNames = new HashSet<string>(
ordinaryRequiredProperties.Select(x => x.Name.ToCamelCase()),
StringComparer.Ordinal);

var choiceParamName = "value";
if (requiredParamNames.Contains(choiceParamName))
{
choiceParamName = "choiceValue";
var suffix = 1;
while (requiredParamNames.Contains(choiceParamName))
{
choiceParamName = $"choiceValue{suffix}";
suffix++;
}
}

// Add parameter descriptions for required ordinary properties
foreach (var reqProp in ordinaryRequiredProperties)
{
var paramName = reqProp.Name.ToCamelCase();
sb.AppendParamDescription(paramName, $"The value for the required property {reqProp.Name}.");
}

sb.AppendParamDescription(choiceParamName, "The value to assign to the created choice instance.");

// Build method signature with required property parameters
sb.Append($"public static {typeFullName} CreateAs").Append(prop.Name).Append("(");

// Add required ordinary properties as parameters first
for (var i = 0; i < ordinaryRequiredProperties.Count; i++)
{
var reqProp = ordinaryRequiredProperties[i];
var paramName = reqProp.Name.ToCamelCase();
var propType = reqProp.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
sb.Append(propType).Append(" ").Append(paramName).Append(", ");
}

// Add the choice property parameter
sb.Append(prop.TypeName).Append(" ").Append(choiceParamName).Append(") => new () { ");

// Initialize required ordinary properties
foreach (var reqProp in ordinaryRequiredProperties)
{
var paramName = reqProp.Name.ToCamelCase();
sb.Append(reqProp.Name).Append(" = ").Append(paramName).Append(", ");
}

// Initialize the choice property
sb.Append(prop.Name).Append(" = ").Append(choiceParamName).AppendLine(" };");

sb.NewLine();
}
Expand All @@ -191,8 +251,17 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp

sb.NewLine();

if (!ProcessImplicitOperators(sb, typeSymbol.Name, processedProperties))
// Skip implicit operators if there are required ordinary properties
// because implicit operators cannot initialize required properties
if (ordinaryRequiredProperties.Count == 0)
{
if (!ProcessImplicitOperators(sb, typeSymbol.Name, processedProperties))
sb.NewLine();
}
else
{
sb.NewLine();
}

// Generate ShouldSerialize methods for all properties to prevent xsi:nil in XML
// This ensures that only the active choice property is serialized
Expand Down
11 changes: 11 additions & 0 deletions src/AltaSoft.Choice.Generator/Extensions/CompilationExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ internal static string ToFieldName(this string s)
return "_" + char.ToLower(s[0], CultureInfo.InvariantCulture) + s.Substring(1);
}

internal static string ToCamelCase(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return s;

if (s.Length == 1)
return s.ToLower(CultureInfo.InvariantCulture);

return char.ToLower(s[0], CultureInfo.InvariantCulture) + s.Substring(1);
}

/// <summary>
/// Returns the C# keyword representation of a property's <see cref="Accessibility"/> level,
/// optionally omitting the keyword for <c>public</c> if <paramref name="emptyOnPublic"/> is true.
Expand Down
106 changes: 106 additions & 0 deletions tests/AltaSoft.Choice.Generator.SnapshotTests/ChoiceGeneratorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,112 @@ public sealed partial class MixedAttributeChoice
});
}

[Fact]
public Task ChoiceTypeShouldGenerateWithRequiredOrdinaryProperties()
{
const string source =
"""
using System;
using AltaSoft.Choice;

namespace TestNamespace
{
[Choice]
public sealed partial class OrderChoice
{
/// <summary>
/// Required ordinary property - Order ID
/// </summary>
public required string OrderId { get; set; }

/// <summary>
/// Required ordinary property - Customer Name
/// </summary>
public required string CustomerName { get; set; }

/// <summary>
/// Choice property - Express delivery
/// </summary>
public partial ExpressDelivery? Express { get; set; }

/// <summary>
/// Choice property - Standard delivery
/// </summary>
public partial StandardDelivery? Standard { get; set; }
}

public sealed class ExpressDelivery
{
public DateTime DeliveryDate { get; set; }
public decimal SurchargeAmount { get; set; }
}

public sealed class StandardDelivery
{
public int DeliveryDays { get; set; }
public decimal ShippingCost { get; set; }
}
}
""";

return TestHelper.Verify(source, (_, x, _) =>
{
Assert.Single(x);
});
}

[Fact]
public Task ChoiceTypeShouldGenerateWithRequiredPropertyNamedValue()
{
const string source =
"""
using System;
using AltaSoft.Choice;

namespace TestNamespace
{
[Choice]
public sealed partial class ConfigChoice
{
/// <summary>
/// Required property named "Value" - tests parameter conflict resolution
/// </summary>
public required string Value { get; set; }

/// <summary>
/// Required property named "ChoiceValue" - tests fallback parameter conflict resolution
/// </summary>
public required string ChoiceValue { get; set; }

/// <summary>
/// Choice property - Option A
/// </summary>
public partial OptionA? OptionA { get; set; }

/// <summary>
/// Choice property - Option B
/// </summary>
public partial OptionB? OptionB { get; set; }
}

public sealed class OptionA
{
public string Data { get; set; }
}

public sealed class OptionB
{
public int Count { get; set; }
}
}
""";

return TestHelper.Verify(source, (_, x, _) =>
{
Assert.Single(x);
});
}

public static class TestHelper
{
internal static Task Verify(string source, Action<ImmutableArray<Diagnostic>, List<string>, GeneratorDriver>? additionalChecks = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ namespace TestNamespace;

#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable IDE0290 // Use primary constructor

public sealed partial class Authorisation1Choice
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ namespace TestNamespace;

#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable IDE0290 // Use primary constructor

public sealed partial class ArrayInTypeChoice
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ namespace TestNamespace;

#pragma warning disable CS8774 // Member must have a non-null value when exiting.
#pragma warning disable CS0628 // New protected member declared in sealed type
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
#pragma warning disable IDE0290 // Use primary constructor

public sealed partial class MixedAttributeChoice
{
Expand Down
Loading
Loading