Skip to content

Commit b32d5ff

Browse files
Fixed a bug in implicit operator generation where referenced type domain primitives were not correctly processed, causing null reference exception. The solution involved checking for null.
1 parent 1d3ee0e commit b32d5ff

File tree

39 files changed

+1657
-22
lines changed

39 files changed

+1657
-22
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<Product>Domain Primitives</Product>
1010
<Company>ALTA Software llc.</Company>
1111
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
12-
<Version>2.0.1</Version>
12+
<Version>2.0.2</Version>
1313
</PropertyGroup>
1414

1515
<PropertyGroup>

src/AltaSoft.DomainPrimitives.Generator/Executor.cs

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -662,51 +662,73 @@ static StringBuilder AppendInterface(StringBuilder sb, string interfaceName)
662662
private static void GenerateImplicitOperators(GeneratorData data, SourceCodeBuilder builder)
663663
{
664664
var friendlyName = data.PrimitiveTypeFriendlyName;
665+
var type = data.PrimitiveTypeSymbol;
666+
var className = data.ClassName;
665667

668+
// From Underlying to our type
666669
if (data.TypeSymbol.IsValueType)
667670
{
668-
builder.AppendSummary($"Implicit conversion from <see cref = \"{friendlyName}\"/> to <see cref = \"{data.ClassName}\"/>")
671+
builder.AppendSummary($"Implicit conversion from <see cref = \"{friendlyName}\"/> to <see cref = \"{className}\"/>")
669672
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
670-
.Append($"public static implicit operator {data.ClassName}({friendlyName} value)")
671-
.AppendLine(" => new(value);")
673+
.Append($"public static implicit operator {className}({friendlyName} value)").AppendLine(" => new(value);")
672674
.NewLine();
673675
}
674676

675-
var type = data.PrimitiveTypeSymbol;
676-
677-
builder.AppendSummary($"Implicit conversion from <see cref = \"{friendlyName}\"/> (nullable) to <see cref = \"{data.ClassName}\"/> (nullable)")
677+
builder.AppendSummary($"Implicit conversion from <see cref = \"{friendlyName}\"/> (nullable) to <see cref = \"{className}\"/> (nullable)")
678678
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
679679
.AppendLine("[return: NotNullIfNotNull(nameof(value))]")
680-
.Append($"public static implicit operator {data.ClassName}?({friendlyName}? value)")
680+
.Append($"public static implicit operator {className}?({friendlyName}? value)")
681681
.AppendLine($" => value is null ? null : new(value{(type.IsValueType ? ".Value" : "")});")
682682
.NewLine();
683683

684+
// From our type to underlying type
685+
if (data.TypeSymbol.IsValueType)
686+
{
687+
builder.AppendSummary($"Implicit conversion from <see cref = \"{className}\"/> to <see cref = \"{friendlyName}\"/>")
688+
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
689+
.Append($"public static implicit operator {friendlyName}({className} value)").AppendLine($" => ({friendlyName})value.{data.FieldName};")
690+
.NewLine();
691+
}
692+
693+
builder.AppendSummary($"Implicit conversion from <see cref = \"{className}\"/> (nullable) to <see cref = \"{friendlyName}\"/> (nullable)")
694+
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
695+
.AppendLine("[return: NotNullIfNotNull(nameof(value))]")
696+
.Append($"public static implicit operator {friendlyName}?({className}? value)")
697+
.AppendLine($" => value is null ? null : ({friendlyName}?)value{(type.IsValueType ? ".Value" : "")}.{data.FieldName};")
698+
.NewLine();
699+
684700
if (data.ParentSymbols.Count != 0)
685701
{
686-
builder.AppendSummary($"Implicit conversion from <see cref = \"{data.ParentSymbols[0].Name}\"/> to <see cref = \"{data.ClassName}\"/>")
702+
var parentClassName = data.ParentSymbols[0].Name;
703+
704+
if (data.TypeSymbol.IsValueType)
705+
{
706+
builder.AppendSummary($"Implicit conversion from <see cref = \"{parentClassName}\"/> to <see cref = \"{className}\"/>")
687707
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
688-
.Append($"public static implicit operator {data.ClassName}({data.ParentSymbols[0].Name} value)")
708+
.Append($"public static implicit operator {className}({parentClassName} value)")
689709
.AppendLine(" => new(value);")
690710
.NewLine();
691-
}
711+
}
692712

693-
builder.AppendSummary($"Implicit conversion from <see cref = \"{data.ClassName}\"/> to <see cref = \"{friendlyName}\"/>")
694-
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
695-
.Append($"public static implicit operator {friendlyName}({data.ClassName} value)")
696-
.AppendLine($" => ({friendlyName})value.{data.FieldName};")
697-
.NewLine();
713+
builder.AppendSummary($"Implicit conversion from <see cref = \"{parentClassName}\"/> (nullable) to <see cref = \"{className}\"/> (nullable)")
714+
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
715+
.AppendLine("[return: NotNullIfNotNull(nameof(value))]")
716+
.Append($"public static implicit operator {className}?({parentClassName}? value)")
717+
.AppendLine($" => value is null ? null : ({className}?)value{(type.IsValueType ? ".Value" : "")}.{data.FieldName};")
718+
.NewLine();
719+
}
698720

699721
if (data.UnderlyingType is DomainPrimitiveUnderlyingType.DateOnly or DomainPrimitiveUnderlyingType.TimeOnly)
700722
{
701-
builder.AppendSummary($"Implicit conversion from <see cref = \"{data.ClassName}\"/> to <see cref = \"DateTime\"/>")
723+
builder.AppendSummary($"Implicit conversion from <see cref = \"{className}\"/> to <see cref = \"DateTime\"/>")
702724
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
703-
.Append($"public static implicit operator DateTime({data.ClassName} value)")
725+
.Append($"public static implicit operator DateTime({className} value)")
704726
.AppendLine($" => (({friendlyName})value.{data.FieldName}).ToDateTime();")
705727
.NewLine();
706728

707-
builder.AppendSummary($"Implicit conversion from <see cref = \"DateTime\"/> to <see cref = \"{data.ClassName}\"/>")
729+
builder.AppendSummary($"Implicit conversion from <see cref = \"DateTime\"/> to <see cref = \"{className}\"/>")
708730
.AppendLine("[MethodImpl(MethodImplOptions.AggressiveInlining)]")
709-
.Append($"public static implicit operator {data.ClassName}(DateTime value)")
731+
.Append($"public static implicit operator {className}(DateTime value)")
710732
.AppendLine($" => {data.UnderlyingType}.FromDateTime(value);")
711733
.NewLine();
712734
}

src/AltaSoft.DomainPrimitives.Generator/Helpers/SourceCodeBuilder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ public SourceCodeBuilder(int startingIndent = 0)
7171
/// Returns the length of the new line character(s) used in the current environment.
7272
/// </summary>
7373
/// <returns>The length of the new line character(s).</returns>
74+
#pragma warning disable CA1822
7475
public int GetNewLineLength() => s_newLineLength;
76+
#pragma warning restore CA1822
7577

7678
/// <summary>
7779
/// Returns a string that represents the specified number of indentation chars.

tests/AltaSoft.DomainPrimitives.Generator.Tests/DomainPrimitiveGeneratorTest.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,95 @@ public static void Validate(char value)
623623
return TestHelper.Verify(source, (_, x, _) => Assert.Equal(4, x.Count));
624624
}
625625

626+
[Fact]
627+
public Task StringOfStringValue_GeneratesAllInterfacesAndConverters()
628+
{
629+
const string source = """
630+
using System;
631+
using System.Collections.Generic;
632+
using System.Linq;
633+
using System.Text;
634+
using System.Threading.Tasks;
635+
using AltaSoft.DomainPrimitives.Abstractions;
636+
637+
namespace AltaSoft.DomainPrimitives;
638+
639+
/// <inheritdoc/>
640+
public partial class StringValue : IDomainValue<string>
641+
{
642+
/// <inheritdoc/>
643+
public static void Validate(string value)
644+
{
645+
if (value=="Test")
646+
throw new InvalidDomainValueException("Invalid Value");
647+
}
648+
649+
/// <inheritdoc/>
650+
public static string Default => default;
651+
}
652+
653+
/// <inheritdoc/>
654+
public partial class StringOfStringValue : IDomainValue<StringValue>
655+
{
656+
/// <inheritdoc/>
657+
public static void Validate(StringValue value)
658+
{
659+
if (value=="Test")
660+
throw new InvalidDomainValueException("Invalid Value");
661+
}
662+
663+
/// <inheritdoc/>
664+
public static StringValue Default => default;
665+
}
666+
""";
667+
668+
return TestHelper.Verify(source, (_, x, _) => Assert.Equal(7, x.Count));
669+
}
670+
671+
[Fact]
672+
public Task IntOfIntValue_GeneratesAllInterfacesAndConverters()
673+
{
674+
const string source = """
675+
using System;
676+
using System.Collections.Generic;
677+
using System.Linq;
678+
using System.Text;
679+
using System.Threading.Tasks;
680+
using AltaSoft.DomainPrimitives.Abstractions;
681+
682+
namespace AltaSoft.DomainPrimitives;
683+
684+
/// <inheritdoc/>
685+
public readonly partial struct IntValue : IDomainValue<int>
686+
{
687+
/// <inheritdoc/>
688+
public static void Validate(int value)
689+
{
690+
if (value < 10 || value > 20)
691+
throw new InvalidDomainValueException("Invalid Value");
692+
}
693+
694+
/// <inheritdoc/>
695+
public static int Default => default;
696+
}
697+
698+
/// <inheritdoc/>
699+
public readonly partial struct IntOfIntValue : IDomainValue<IntValue>
700+
{
701+
/// <inheritdoc/>
702+
public static void Validate(IntValue value)
703+
{
704+
if (value < 10 || value > 20)
705+
throw new InvalidDomainValueException("Invalid Value");
706+
}
707+
708+
/// <inheritdoc/>
709+
public static IntValue Default => default;
710+
}
711+
""";
712+
713+
return TestHelper.Verify(source, (_, x, _) => Assert.Equal(7, x.Count));
714+
}
626715
public static class TestHelper
627716
{
628717
internal static Task Verify(string source, Action<ImmutableArray<Diagnostic>, List<string>, GeneratorDriver>? additionalChecks = null, DomainPrimitiveGlobalOptions? options = null)

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.BoolValue_GeneratesAllInterfacesAndConverters#BoolValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ public int CompareTo(object? obj)
9595
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9696
public static implicit operator bool(BoolValue value) => (bool)value._value;
9797

98+
/// <summary>
99+
/// Implicit conversion from <see cref = "BoolValue"/> (nullable) to <see cref = "bool"/> (nullable)
100+
/// </summary>
101+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
102+
[return: NotNullIfNotNull(nameof(value))]
103+
public static implicit operator bool?(BoolValue? value) => value is null ? null : (bool?)value.Value._value;
104+
98105
/// <inheritdoc/>
99106
[MethodImpl(MethodImplOptions.AggressiveInlining)]
100107
public static BoolValue Parse(string s, IFormatProvider? provider) => bool.Parse(s);

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.ByteValue_GeneratesAllInterfacesAndConverters#ByteValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ public int CompareTo(object? obj)
9999
[MethodImpl(MethodImplOptions.AggressiveInlining)]
100100
public static implicit operator byte(ByteValue value) => (byte)value._value;
101101

102+
/// <summary>
103+
/// Implicit conversion from <see cref = "ByteValue"/> (nullable) to <see cref = "byte"/> (nullable)
104+
/// </summary>
105+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106+
[return: NotNullIfNotNull(nameof(value))]
107+
public static implicit operator byte?(ByteValue? value) => value is null ? null : (byte?)value.Value._value;
108+
102109
/// <inheritdoc/>
103110
[MethodImpl(MethodImplOptions.AggressiveInlining)]
104111
public static bool operator <(ByteValue left, ByteValue right) => left._value < right._value;

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.CharValue_GeneratesAllInterfacesAndConverters#CharValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ public int CompareTo(object? obj)
9999
[MethodImpl(MethodImplOptions.AggressiveInlining)]
100100
public static implicit operator char(CharValue value) => (char)value._value;
101101

102+
/// <summary>
103+
/// Implicit conversion from <see cref = "CharValue"/> (nullable) to <see cref = "char"/> (nullable)
104+
/// </summary>
105+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106+
[return: NotNullIfNotNull(nameof(value))]
107+
public static implicit operator char?(CharValue? value) => value is null ? null : (char?)value.Value._value;
108+
102109
/// <inheritdoc/>
103110
[MethodImpl(MethodImplOptions.AggressiveInlining)]
104111
public static bool operator <(CharValue left, CharValue right) => left._value < right._value;

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DateOnlyValue_GeneratesAllInterfacesAndConverters#DateOnlyValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@ public int CompareTo(object? obj)
101101
[MethodImpl(MethodImplOptions.AggressiveInlining)]
102102
public static implicit operator DateOnly(DateOnlyValue value) => (DateOnly)value._value;
103103

104+
/// <summary>
105+
/// Implicit conversion from <see cref = "DateOnlyValue"/> (nullable) to <see cref = "DateOnly"/> (nullable)
106+
/// </summary>
107+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
108+
[return: NotNullIfNotNull(nameof(value))]
109+
public static implicit operator DateOnly?(DateOnlyValue? value) => value is null ? null : (DateOnly?)value.Value._value;
110+
104111
/// <summary>
105112
/// Implicit conversion from <see cref = "DateOnlyValue"/> to <see cref = "DateTime"/>
106113
/// </summary>

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DateTimeOffsetValue_GeneratesAllInterfacesAndConverters#DateTimeOffsetValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ public int CompareTo(object? obj)
100100
[MethodImpl(MethodImplOptions.AggressiveInlining)]
101101
public static implicit operator DateTimeOffset(DateTimeOffsetValue value) => (DateTimeOffset)value._value;
102102

103+
/// <summary>
104+
/// Implicit conversion from <see cref = "DateTimeOffsetValue"/> (nullable) to <see cref = "DateTimeOffset"/> (nullable)
105+
/// </summary>
106+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
107+
[return: NotNullIfNotNull(nameof(value))]
108+
public static implicit operator DateTimeOffset?(DateTimeOffsetValue? value) => value is null ? null : (DateTimeOffset?)value.Value._value;
109+
103110
/// <inheritdoc/>
104111
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105112
public static bool operator <(DateTimeOffsetValue left, DateTimeOffsetValue right) => left._value < right._value;

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DateTimeValue_GeneratesAllInterfacesAndConverters#DateTimeValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ public int CompareTo(object? obj)
100100
[MethodImpl(MethodImplOptions.AggressiveInlining)]
101101
public static implicit operator DateTime(DateTimeValue value) => (DateTime)value._value;
102102

103+
/// <summary>
104+
/// Implicit conversion from <see cref = "DateTimeValue"/> (nullable) to <see cref = "DateTime"/> (nullable)
105+
/// </summary>
106+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
107+
[return: NotNullIfNotNull(nameof(value))]
108+
public static implicit operator DateTime?(DateTimeValue? value) => value is null ? null : (DateTime?)value.Value._value;
109+
103110
/// <inheritdoc/>
104111
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105112
public static bool operator <(DateTimeValue left, DateTimeValue right) => left._value < right._value;

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DecimalValue_GeneratesAllInterfacesAndConverters#DecimalValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public int CompareTo(object? obj)
104104
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105105
public static implicit operator decimal(DecimalValue value) => (decimal)value._value;
106106

107+
/// <summary>
108+
/// Implicit conversion from <see cref = "DecimalValue"/> (nullable) to <see cref = "decimal"/> (nullable)
109+
/// </summary>
110+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
111+
[return: NotNullIfNotNull(nameof(value))]
112+
public static implicit operator decimal?(DecimalValue? value) => value is null ? null : (decimal?)value.Value._value;
113+
107114
/// <inheritdoc/>
108115
[MethodImpl(MethodImplOptions.AggressiveInlining)]
109116
public static DecimalValue operator +(DecimalValue left, DecimalValue right) => new(left._value + right._value);

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.DoubleValue_GeneratesAllInterfacesAndConverters#DoubleValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public int CompareTo(object? obj)
104104
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105105
public static implicit operator double(DoubleValue value) => (double)value._value;
106106

107+
/// <summary>
108+
/// Implicit conversion from <see cref = "DoubleValue"/> (nullable) to <see cref = "double"/> (nullable)
109+
/// </summary>
110+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
111+
[return: NotNullIfNotNull(nameof(value))]
112+
public static implicit operator double?(DoubleValue? value) => value is null ? null : (double?)value.Value._value;
113+
107114
/// <inheritdoc/>
108115
[MethodImpl(MethodImplOptions.AggressiveInlining)]
109116
public static DoubleValue operator +(DoubleValue left, DoubleValue right) => new(left._value + right._value);

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.FloatValue_GeneratesAllInterfacesAndConverters#FloatValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public int CompareTo(object? obj)
104104
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105105
public static implicit operator float(FloatValue value) => (float)value._value;
106106

107+
/// <summary>
108+
/// Implicit conversion from <see cref = "FloatValue"/> (nullable) to <see cref = "float"/> (nullable)
109+
/// </summary>
110+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
111+
[return: NotNullIfNotNull(nameof(value))]
112+
public static implicit operator float?(FloatValue? value) => value is null ? null : (float?)value.Value._value;
113+
107114
/// <inheritdoc/>
108115
[MethodImpl(MethodImplOptions.AggressiveInlining)]
109116
public static FloatValue operator +(FloatValue left, FloatValue right) => new(left._value + right._value);

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.GuidValue_GeneratesAllInterfacesAndConverters#GuidValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ public int CompareTo(object? obj)
9898
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9999
public static implicit operator Guid(GuidValue value) => (Guid)value._value;
100100

101+
/// <summary>
102+
/// Implicit conversion from <see cref = "GuidValue"/> (nullable) to <see cref = "Guid"/> (nullable)
103+
/// </summary>
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
[return: NotNullIfNotNull(nameof(value))]
106+
public static implicit operator Guid?(GuidValue? value) => value is null ? null : (Guid?)value.Value._value;
107+
101108
/// <inheritdoc/>
102109
[MethodImpl(MethodImplOptions.AggressiveInlining)]
103110
public static GuidValue Parse(string s, IFormatProvider? provider) => Guid.Parse(s, provider);

tests/AltaSoft.DomainPrimitives.Generator.Tests/Snapshots/DomainPrimitiveGeneratorTest.Int16Value_GeneratesAllInterfacesAndConverters#ShortValue.g.verified.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ public int CompareTo(object? obj)
9999
[MethodImpl(MethodImplOptions.AggressiveInlining)]
100100
public static implicit operator short(ShortValue value) => (short)value._value;
101101

102+
/// <summary>
103+
/// Implicit conversion from <see cref = "ShortValue"/> (nullable) to <see cref = "short"/> (nullable)
104+
/// </summary>
105+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
106+
[return: NotNullIfNotNull(nameof(value))]
107+
public static implicit operator short?(ShortValue? value) => value is null ? null : (short?)value.Value._value;
108+
102109
/// <inheritdoc/>
103110
[MethodImpl(MethodImplOptions.AggressiveInlining)]
104111
public static bool operator <(ShortValue left, ShortValue right) => left._value < right._value;

0 commit comments

Comments
 (0)