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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<Product>Choice generator</Product>
<Company>ALTA Software llc.</Company>
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
<Version>1.3.3</Version>
<Version>1.3.4</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
8 changes: 3 additions & 5 deletions src/AltaSoft.Choice.Generator/Executor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,8 @@ private static SourceCodeBuilder Process(INamedTypeSymbol typeSymbol, List<IProp

foreach (var prop in processedProperties)
{

var typeFullName = typeSymbol.GetFullName();
sb.AppendSummary($"Creates a new <see cref=\"{typeFullName}\"/> instance " +
$"and sets its value using the specified <see cref=\"{prop.TypeName}\"/>.");
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("(")
Expand Down Expand Up @@ -231,8 +229,8 @@ private static bool ProcessImplicitOperators(SourceCodeBuilder sb, string typeNa

foreach (var property in processedProperties)
{
sb.AppendSummary($"Implicitly converts an <see cref=\"{property.TypeName}\"/> to an <see cref=\"{typeName}\"/>.");
sb.AppendParamDescription("value", $"The <see cref=\"{property.TypeName}\"/> to convert.");
sb.AppendSummary($"Implicitly converts an {property.TypeSymbol.GetCrefForType()} to an <see cref=\"{typeName}\"/>.");
sb.AppendParamDescription("value", $"The {property.TypeSymbol.GetCrefForType()} to convert.");
sb.AppendBlock("returns", $"<see cref=\"{typeName}\"/> instance representing the code.").NewLine();
sb.AppendLine("[return: NotNullIfNotNull(parameterName: nameof(value))]");

Expand Down
20 changes: 20 additions & 0 deletions src/AltaSoft.Choice.Generator/Extensions/CompilationExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ public static bool IsNullableValueType(this INamedTypeSymbol type, out ITypeSymb
return false;
}

/// <summary>
/// Builds a short XML documentation fragment that references the provided <paramref name="type"/> using a <see cref="object"/> cref tag.
/// For array types the fragment references the element type and appends the word "array." to indicate the collection nature
/// (for example: &lt;see cref="System.String"/&gt; array.). For non-array types the fragment is simply the cref reference followed by a period.
/// </summary>
/// <param name="type">The Roslyn <see cref="ITypeSymbol"/> to create a cref for. This should not be <c>null</c>.</param>
/// <returns>
/// A string containing an XML &lt;see cref="System.Object" /&gt; tag that references the type. Example results:
/// - For a type: "&lt;see cref="Namespace.TypeName"/&gt;."
/// - For an array: "&lt;see cref="Namespace.ElementType"/&gt; array."
/// </returns>
public static string GetCrefForType(this ITypeSymbol type)
{
if (type is IArrayTypeSymbol array)
{
return $"<see cref=\"{array.ElementType.GetFullName()}\"/> array";
}
return $"<see cref=\"{type.GetFullName()}\"/>";
}

/// <summary>
/// A dictionary that provides aliases for common .NET framework types, mapping their full names to shorter aliases.
/// </summary>
Expand Down
6 changes: 0 additions & 6 deletions src/AltaSoft.Choice.Generator/Helpers/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,4 @@ internal static class Constants
/// This constant is used to identify the <c>XmlTagAttribute</c> type by its name.
/// </summary>
internal const string XmlTagAttributeFullName = "AltaSoft.Choice.XmlTagAttribute";

/// <summary>
/// The fully qualified name of the <c>IXmlSerializable</c> interface.
/// This constant is used to identify the <c>IXmlSerializable</c> type by its name.
/// </summary>
internal const string IXmlSerializableFullName = "IXmlSerializable";
}
4 changes: 2 additions & 2 deletions src/AltaSoft.Choice.Generator/Models/PropertyDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ internal sealed class PropertyDetails
/// <summary>
/// The type symbol of the property.
/// </summary>
internal ITypeSymbol TypeSymbol { get; private set; }
internal ITypeSymbol TypeSymbol { get; }

/// <summary>
/// returns if the type is dateOnly
/// </summary>
internal bool IsDateOnly()
{
return TypeSymbol.IsValueType && TypeSymbol.GetFullName()?.Replace("?", "") == "System.DateOnly";
return TypeSymbol.IsValueType && TypeSymbol.GetFullName().Replace("?", "") == "System.DateOnly";
}
/// <summary>
/// Initializes a new instance of the <see cref="PropertyDetails"/> class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,37 @@ public enum Authorisation1Code
Assert.Single(x);
});
}
[Fact]
public Task ChoiceTypeShouldGenerateDocumentationCorrectly_ForArrayInChoice()
{
const string source =
"""
using System;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using AltaSoft.Choice;

namespace TestNamespace
{
[Choice]
public sealed partial class ArrayInTypeChoice
{
public partial string? StringChoice { get; set; }

public partial AccountId[]? Accounts { get; set; }
}

public sealed record AccountId(int Id);
}

""";

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

[Fact]
public Task ChoiceTypeShouldNotGenerateImplicitMethodsAndCompileCorrectly()
Expand Down Expand Up @@ -120,7 +151,7 @@ public static class TestHelper
internal static Task Verify(string source, Action<ImmutableArray<Diagnostic>, List<string>, GeneratorDriver>? additionalChecks = null)
{
List<Assembly> assemblies = [typeof(XmlElementAttribute).Assembly, typeof(JsonSerializer).Assembly];
var (diagnostics, output, driver) = TestHelpers.GetGeneratedOutput<AltaSoft.Choice.Generator.ChoiceGenerator>(source, assemblies);
var (diagnostics, output, driver) = TestHelpers.GetGeneratedOutput<ChoiceGenerator>(source, assemblies);

Assert.Empty(diagnostics.Where(x => x.Severity == DiagnosticSeverity.Error));
additionalChecks?.Invoke(diagnostics, output, driver);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using DiffEngine;
using VerifyTests;

namespace AltaSoft.Choice.Generator.SnapshotTests;
Expand All @@ -8,6 +9,7 @@ public static class ModuleInitializer
[ModuleInitializer]
public static void Init()
{
DiffTools.UseOrder(DiffTool.VisualStudioCode, DiffTool.VisualStudio, DiffTool.AraxisMerge, DiffTool.BeyondCompare);
VerifySourceGenerators.Initialize();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//HintName: ArrayInTypeChoice.g.cs
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by 'AltaSoft Choice.Generator'.
// Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

#nullable enable

using TestNamespace;
using AltaSoft.Choice;
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Schema;

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

public sealed partial class ArrayInTypeChoice
{
/// <summary>
/// Constructor for Serialization/Deserialization
/// </summary>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public ArrayInTypeChoice()
{
}

/// <summary>
/// <para>Choice enum </para>
/// </summary>
[JsonIgnore]
[XmlIgnore]
[ChoiceTypeProperty]
public ChoiceOf ChoiceType { get; private set; }

private string? _stringChoice;

[DisallowNull]
[XmlElement("StringChoice")]
[ChoiceProperty]
public partial string? StringChoice
{
get => _stringChoice;
set
{
_stringChoice = value ?? throw new InvalidOperationException("Choice value cannot be null");
_accounts = null;
ChoiceType = ChoiceOf.StringChoice;
}
}

private TestNamespace.AccountId[]? _accounts;

[DisallowNull]
[XmlElement("Accounts")]
[ChoiceProperty]
public partial TestNamespace.AccountId[]? Accounts
{
get => _accounts;
set
{
_accounts = value ?? throw new InvalidOperationException("Choice value cannot be null");
_stringChoice = null;
ChoiceType = ChoiceOf.Accounts;
}
}


/// <summary>
/// Creates a new <see cref="TestNamespace.ArrayInTypeChoice"/> instance and sets its value using the specified <see cref="string"/>.
/// </summary>
/// <param name="value">The value to assign to the created choice instance.</param>
public static TestNamespace.ArrayInTypeChoice CreateAsStringChoice(string value) => new () { StringChoice = value };

/// <summary>
/// Creates a new <see cref="TestNamespace.ArrayInTypeChoice"/> instance and sets its value using the specified <see cref="TestNamespace.AccountId"/> array.
/// </summary>
/// <param name="value">The value to assign to the created choice instance.</param>
public static TestNamespace.ArrayInTypeChoice CreateAsAccounts(TestNamespace.AccountId[] value) => new () { Accounts = value };

/// <summary>
/// <para>Applies the appropriate function based on the current choice type</para>
/// </summary>
/// <typeparam name="TResult">The return type of the provided match functions</typeparam>
/// <param name="matchStringChoice">Function to invoke if the choice is a <see cref="ChoiceOf.StringChoice"/> value</param>
/// <param name="matchAccounts">Function to invoke if the choice is a <see cref="ChoiceOf.Accounts"/> value</param>
public TResult Match<TResult>(
Func<string, TResult> matchStringChoice,
Func<TestNamespace.AccountId[], TResult> matchAccounts)
{
return ChoiceType switch
{
ChoiceOf.StringChoice => matchStringChoice(StringChoice!),
ChoiceOf.Accounts => matchAccounts(Accounts!),
_ => throw new InvalidOperationException($"Invalid ChoiceType. '{ChoiceType}'")
};
}

/// <summary>
/// <para>Applies the appropriate Action based on the current choice type</para>
/// </summary>
/// <param name="matchStringChoice">Action to invoke if the choice is a <see cref="ChoiceOf.StringChoice"/> value</param>
/// <param name="matchAccounts">Action to invoke if the choice is a <see cref="ChoiceOf.Accounts"/> value</param>
public void Switch(
Action<string> matchStringChoice,
Action<TestNamespace.AccountId[]> matchAccounts)
{
switch (ChoiceType)
{
case ChoiceOf.StringChoice:
matchStringChoice(StringChoice!);
return;

case ChoiceOf.Accounts:
matchAccounts(Accounts!);
return;

default:
throw new XmlException($"Invalid ChoiceType. '{ChoiceType}'");
}
}

/// <summary>
/// Implicitly converts an <see cref="string"/> to an <see cref="ArrayInTypeChoice"/>.
/// </summary>
/// <param name="value">The <see cref="string"/> to convert.</param>
/// <returns>
/// <see cref="ArrayInTypeChoice"/> instance representing the code.
/// </returns>

[return: NotNullIfNotNull(parameterName: nameof(value))]
public static implicit operator ArrayInTypeChoice? (string? value)
{
return value is null ? null : CreateAsStringChoice(value);
}

/// <summary>
/// Implicitly converts an <see cref="TestNamespace.AccountId"/> array to an <see cref="ArrayInTypeChoice"/>.
/// </summary>
/// <param name="value">The <see cref="TestNamespace.AccountId"/> array to convert.</param>
/// <returns>
/// <see cref="ArrayInTypeChoice"/> instance representing the code.
/// </returns>

[return: NotNullIfNotNull(parameterName: nameof(value))]
public static implicit operator ArrayInTypeChoice? (TestNamespace.AccountId[]? value)
{
return value is null ? null : CreateAsAccounts(value);
}

/// <summary>
/// <para>Choice enumeration</para>
/// </summary>
[XmlType("ChoiceOf.ArrayInTypeChoice")]
public enum ChoiceOf
{
StringChoice,
Accounts,
}
}
13 changes: 13 additions & 0 deletions tests/AltaSoft.ChoiceGenerator.Tests/ArrayInTypeChoice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using AltaSoft.Choice;

namespace AltaSoft.ChoiceGenerator.Tests;

[Choice]
public sealed partial class ArrayInTypeChoice
{
public partial string? StringChoice { get; set; }

public partial AccountId[]? Accounts { get; set; }
}

public sealed record AccountId(int Id);