diff --git a/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs b/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs index fd5ba16d40..bf68f4ce20 100644 --- a/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/AotConverterGenerator.cs @@ -73,6 +73,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return null; } + // Skip conversion operators where the containing type is not publicly accessible + // The generated code won't be able to reference private/internal types + if (containingType.DeclaredAccessibility != Accessibility.Public) + { + return null; + } + + // Also skip if the target type of the conversion is not publicly accessible + // (unless it's a built-in type) + if (targetType is INamedTypeSymbol namedTargetType && + namedTargetType.SpecialType == SpecialType.None && + namedTargetType.DeclaredAccessibility != Accessibility.Public) + { + return null; + } + return new ConversionInfo { ContainingType = containingType, diff --git a/TUnit.TestProject/Bugs/Issue2993/CompilationFailureTests.cs b/TUnit.TestProject/Bugs/Issue2993/CompilationFailureTests.cs new file mode 100644 index 0000000000..c444ea6fe5 --- /dev/null +++ b/TUnit.TestProject/Bugs/Issue2993/CompilationFailureTests.cs @@ -0,0 +1,29 @@ +namespace TUnit.TestProject.Bugs.Issue2993; + +internal class CompilationFailureTests +{ + private record TestItem(int? Value) + { + public static implicit operator TestItem(int? value) => new(value); + } + + [Test] + public async Task AssertEmptyIsEmpty() + { + var items = Enumerable.Empty(); + await Assert.That(items).IsEmpty(); + } + + // Also test with non-nullable int to verify both work + private record TestItem2(int Value) + { + public static implicit operator TestItem2(int value) => new(value); + } + + [Test] + public async Task AssertEmptyIsEmpty_NonNullable() + { + var items = Enumerable.Empty(); + await Assert.That(items).IsEmpty(); + } +} \ No newline at end of file diff --git a/TUnit.TestProject/Bugs/Issue2993/ImplicitConversionTests.cs b/TUnit.TestProject/Bugs/Issue2993/ImplicitConversionTests.cs new file mode 100644 index 0000000000..2d738e366d --- /dev/null +++ b/TUnit.TestProject/Bugs/Issue2993/ImplicitConversionTests.cs @@ -0,0 +1,79 @@ +namespace TUnit.TestProject.Bugs.Issue2993; + +/// +/// Tests for issue #2993: Private types with implicit int? operator cause compilation failure +/// https://github.com/thomhurst/TUnit/issues/2993 +/// +internal class ImplicitConversionTests +{ + // Test with nullable value type + private record NullableIntRecord(int? Value) + { + public static implicit operator NullableIntRecord(int? value) => new(value); + public static implicit operator int?(NullableIntRecord record) => record?.Value; + } + + [Test] + public async Task PrivateType_WithNullableIntImplicitOperator_ShouldCompile() + { + var items = Enumerable.Empty(); + await Assert.That(items).IsEmpty(); + } + + [Test] + public async Task PrivateType_WithNullableIntImplicitOperator_NonEmptyCollection() + { + NullableIntRecord item1 = 42; + NullableIntRecord item2 = null; + var items = new[] { item1, item2 }; + + await Assert.That(items).IsNotEmpty(); + await Assert.That(items).HasCount(2); + } + + // Test with non-nullable value type + private record IntRecord(int Value) + { + public static implicit operator IntRecord(int value) => new(value); + public static implicit operator int(IntRecord record) => record.Value; + } + + [Test] + public async Task PrivateType_WithIntImplicitOperator_ShouldCompile() + { + var items = Enumerable.Empty(); + await Assert.That(items).IsEmpty(); + } + + // Test with nullable reference type + private record StringRecord(string? Value) + { + public static implicit operator StringRecord(string? value) => new(value); + public static implicit operator string?(StringRecord record) => record?.Value; + } + + [Test] + public async Task PrivateType_WithNullableStringImplicitOperator_ShouldCompile() + { + var items = Enumerable.Empty(); + await Assert.That(items).IsEmpty(); + } + + // Test with nested private type + private class OuterClass + { + internal record InnerRecord(double? Value) + { + public static implicit operator InnerRecord(double? value) => new(value); + } + + public static IEnumerable GetEmptyCollection() => Enumerable.Empty(); + } + + [Test] + public async Task NestedPrivateType_WithImplicitOperator_ShouldCompile() + { + var items = OuterClass.GetEmptyCollection(); + await Assert.That(items).IsEmpty(); + } +} \ No newline at end of file