diff --git a/src/Rocks.CodeGenerationTest/Program.cs b/src/Rocks.CodeGenerationTest/Program.cs index 88a0cd39..9780c0ba 100644 --- a/src/Rocks.CodeGenerationTest/Program.cs +++ b/src/Rocks.CodeGenerationTest/Program.cs @@ -14,9 +14,9 @@ //TestTypeValidity(); //TestWithCode(); -//TestWithType(); +TestWithType(); //TestWithTypeNoEmit(); -TestWithTypes(); +//TestWithTypes(); //TestTypesIndividually(); stopwatch.Stop(); @@ -73,7 +73,7 @@ static void TestWithType() #pragma warning disable EF1001 // Internal EF Core API usage. (var issues, var times) = TestGenerator.Generate(new RockAttributeGenerator(), - [typeof(System.Reactive.Notification<>)], + [typeof(Microsoft.Kiota.Abstractions.Serialization.ISerializationWriter)], typesToLoadAssembliesFrom, MappedTypes.GetMappedTypes(), [], BuildType.Create); diff --git a/src/Rocks.Tests/Generators/OpenGenericsGeneratorTests.cs b/src/Rocks.Tests/Generators/OpenGenericsGeneratorTests.cs index a96cc80f..c25f0bc3 100644 --- a/src/Rocks.Tests/Generators/OpenGenericsGeneratorTests.cs +++ b/src/Rocks.Tests/Generators/OpenGenericsGeneratorTests.cs @@ -1876,6 +1876,229 @@ public static async Task CreateWithNullableReferenceAsync() using Rocks; using System; + [assembly: RockCreate(typeof(INotification))] + + #nullable enable + + public interface INotification + { + void Similar(string? key, T? value) where T : struct, Enum; + } + """; + + var generatedCode = + """ + // + + #nullable enable + + using Rocks.Extensions; + + internal sealed class INotificationCreateExpectations + : global::Rocks.Expectations + { + #pragma warning disable CS8618 + internal sealed class Handler0 + : global::Rocks.Handler> + where T : struct, global::System.Enum + { + public global::Rocks.Argument @key { get; set; } + public global::Rocks.Argument @value { get; set; } + } + private global::Rocks.Handlers? @handlers0; + #pragma warning restore CS8618 + + public override void Verify() + { + if (this.WasInstanceInvoked) + { + var failures = new global::System.Collections.Generic.List(); + + if (this.handlers0 is not null) { failures.AddRange(this.Verify(this.handlers0, 0)); } + + if (failures.Count > 0) + { + throw new global::Rocks.Exceptions.VerificationException(failures); + } + } + } + + private sealed class Mock + : global::INotification + { + public Mock(global::INotificationCreateExpectations @expectations) + { + this.Expectations = @expectations; + } + + [global::Rocks.MemberIdentifier(0)] + public void Similar(string? @key, T? @value) + where T : struct, global::System.Enum + { + if (this.Expectations.handlers0 is not null) + { + var @foundMatch = false; + + foreach (var @genericHandler in this.Expectations.handlers0) + { + if (@genericHandler is global::INotificationCreateExpectations.Handler0 @handler) + { + if (@handler.@key.IsValid(@key!) && + @handler.@value.IsValid(@value!)) + { + @foundMatch = true; + @handler.CallCount++; + @handler.Callback?.Invoke(@key!, @value!); + break; + } + } + } + + if (!@foundMatch) + { + throw new global::Rocks.Exceptions.ExpectationException($"No handlers match for {this.GetType().GetMemberDescription(0)}"); + } + } + else + { + throw new global::Rocks.Exceptions.ExpectationException($"No handlers were found for {this.GetType().GetMemberDescription(0)}"); + } + } + + private global::INotificationCreateExpectations Expectations { get; } + } + + internal sealed class MethodExpectations + { + internal MethodExpectations(global::INotificationCreateExpectations expectations) => + this.Expectations = expectations; + + internal global::INotificationCreateExpectations.Adornments.AdornmentsForHandler0 Similar(global::Rocks.Argument @key, global::Rocks.Argument @value) where T : struct, global::System.Enum + { + global::Rocks.Exceptions.ExpectationException.ThrowIf(this.Expectations.WasInstanceInvoked); + global::System.ArgumentNullException.ThrowIfNull(@key); + global::System.ArgumentNullException.ThrowIfNull(@value); + + var @handler = new global::INotificationCreateExpectations.Handler0 + { + @key = @key, + @value = @value, + }; + + if (this.Expectations.handlers0 is null) { this.Expectations.handlers0 = new(@handler); } + else { this.Expectations.handlers0.Add(@handler); } + return new(@handler); + } + + private global::INotificationCreateExpectations Expectations { get; } + } + + internal global::INotificationCreateExpectations.MethodExpectations Methods { get; } + + internal INotificationCreateExpectations() => + (this.Methods) = (new(this)); + + internal global::INotification Instance() + { + if (!this.WasInstanceInvoked) + { + this.WasInstanceInvoked = true; + var @mock = new Mock(this); + this.MockType = @mock.GetType(); + return @mock; + } + else + { + throw new global::Rocks.Exceptions.NewMockInstanceException("Can only create a new mock once."); + } + } + + internal static class Adornments + { + public interface IAdornmentsForINotification + : global::Rocks.IAdornments + where TAdornments : IAdornmentsForINotification + { } + + public sealed class AdornmentsForHandler0 + : global::Rocks.Adornments, global::INotificationCreateExpectations.Handler0, global::System.Action>, IAdornmentsForINotification> where T : struct, global::System.Enum + { + public AdornmentsForHandler0(global::INotificationCreateExpectations.Handler0 handler) + : base(handler) { } + } + } + } + + """; + + await TestAssistants.RunGeneratorAsync(code, + [(typeof(RockAttributeGenerator), "INotification_Rock_Create.g.cs", generatedCode)], + []); + } + + [Test] + public static async Task MakeWithNullableReferenceAsync() + { + var code = + """ + using Rocks; + using System; + + [assembly: RockMake(typeof(INotification))] + + #nullable enable + + public interface INotification + { + void Similar(string? key, T? value) where T : struct, Enum; + } + """; + + var generatedCode = + """ + // + + #pragma warning disable CS8775 + #nullable enable + + internal sealed class INotificationMakeExpectations + { + internal global::INotification Instance() + { + return new Mock(); + } + + private sealed class Mock + : global::INotification + { + public Mock() + { + } + + public void Similar(string? @key, T? @value) + where T : struct, global::System.Enum + { + } + } + } + + #pragma warning restore CS8775 + + """; + + await TestAssistants.RunGeneratorAsync(code, + [(typeof(RockAttributeGenerator), "INotification_Rock_Make.g.cs", generatedCode)], + []); + } + + [Test] + public static async Task CreateWithGenericNullableReferenceAsync() + { + var code = + """ + using Rocks; + using System; + [assembly: RockCreate(typeof(INotification<>))] #nullable enable @@ -2021,7 +2244,7 @@ public AdornmentsForHandler0(global::INotificationCreateExpectations.Handler0 } [Test] - public static async Task MakeWithNullableReferenceAsync() + public static async Task MakeWithGenericNullableReferenceAsync() { var code = """ @@ -2074,4 +2297,229 @@ public bool Similar(global::INotification? @other) [(typeof(RockAttributeGenerator), "INotificationT_Rock_Make.g.cs", generatedCode)], []); } + + [Test] + public static async Task CreateWithMultipleNullableAnnotationsOnTypeAsync() + { + var code = + """ + using Rocks; + using System; + using System.Collections.Generic; + + [assembly: RockCreate(typeof(IWriter))] + + #nullable enable + + public interface IWriter + { + void WriteCollectionOfEnumValues(string? key, IEnumerable? values) where T : struct, Enum; + } + """; + + var generatedCode = + """ + // + + #nullable enable + + using Rocks.Extensions; + + internal sealed class IWriterCreateExpectations + : global::Rocks.Expectations + { + #pragma warning disable CS8618 + internal sealed class Handler0 + : global::Rocks.Handler?>> + where T : struct, global::System.Enum + { + public global::Rocks.Argument @key { get; set; } + public global::Rocks.Argument?> @values { get; set; } + } + private global::Rocks.Handlers? @handlers0; + #pragma warning restore CS8618 + + public override void Verify() + { + if (this.WasInstanceInvoked) + { + var failures = new global::System.Collections.Generic.List(); + + if (this.handlers0 is not null) { failures.AddRange(this.Verify(this.handlers0, 0)); } + + if (failures.Count > 0) + { + throw new global::Rocks.Exceptions.VerificationException(failures); + } + } + } + + private sealed class Mock + : global::IWriter + { + public Mock(global::IWriterCreateExpectations @expectations) + { + this.Expectations = @expectations; + } + + [global::Rocks.MemberIdentifier(0)] + public void WriteCollectionOfEnumValues(string? @key, global::System.Collections.Generic.IEnumerable? @values) + where T : struct, global::System.Enum + { + if (this.Expectations.handlers0 is not null) + { + var @foundMatch = false; + + foreach (var @genericHandler in this.Expectations.handlers0) + { + if (@genericHandler is global::IWriterCreateExpectations.Handler0 @handler) + { + if (@handler.@key.IsValid(@key!) && + @handler.@values.IsValid(@values!)) + { + @foundMatch = true; + @handler.CallCount++; + @handler.Callback?.Invoke(@key!, @values!); + break; + } + } + } + + if (!@foundMatch) + { + throw new global::Rocks.Exceptions.ExpectationException($"No handlers match for {this.GetType().GetMemberDescription(0)}"); + } + } + else + { + throw new global::Rocks.Exceptions.ExpectationException($"No handlers were found for {this.GetType().GetMemberDescription(0)}"); + } + } + + private global::IWriterCreateExpectations Expectations { get; } + } + + internal sealed class MethodExpectations + { + internal MethodExpectations(global::IWriterCreateExpectations expectations) => + this.Expectations = expectations; + + internal global::IWriterCreateExpectations.Adornments.AdornmentsForHandler0 WriteCollectionOfEnumValues(global::Rocks.Argument @key, global::Rocks.Argument?> @values) where T : struct, global::System.Enum + { + global::Rocks.Exceptions.ExpectationException.ThrowIf(this.Expectations.WasInstanceInvoked); + global::System.ArgumentNullException.ThrowIfNull(@key); + global::System.ArgumentNullException.ThrowIfNull(@values); + + var @handler = new global::IWriterCreateExpectations.Handler0 + { + @key = @key, + @values = @values, + }; + + if (this.Expectations.handlers0 is null) { this.Expectations.handlers0 = new(@handler); } + else { this.Expectations.handlers0.Add(@handler); } + return new(@handler); + } + + private global::IWriterCreateExpectations Expectations { get; } + } + + internal global::IWriterCreateExpectations.MethodExpectations Methods { get; } + + internal IWriterCreateExpectations() => + (this.Methods) = (new(this)); + + internal global::IWriter Instance() + { + if (!this.WasInstanceInvoked) + { + this.WasInstanceInvoked = true; + var @mock = new Mock(this); + this.MockType = @mock.GetType(); + return @mock; + } + else + { + throw new global::Rocks.Exceptions.NewMockInstanceException("Can only create a new mock once."); + } + } + + internal static class Adornments + { + public interface IAdornmentsForIWriter + : global::Rocks.IAdornments + where TAdornments : IAdornmentsForIWriter + { } + + public sealed class AdornmentsForHandler0 + : global::Rocks.Adornments, global::IWriterCreateExpectations.Handler0, global::System.Action?>>, IAdornmentsForIWriter> where T : struct, global::System.Enum + { + public AdornmentsForHandler0(global::IWriterCreateExpectations.Handler0 handler) + : base(handler) { } + } + } + } + + """; + + await TestAssistants.RunGeneratorAsync(code, + [(typeof(RockAttributeGenerator), "IWriter_Rock_Create.g.cs", generatedCode)], + []); + } + + [Test] + public static async Task MakeWithMultipleNullableAnnotationsOnTypeAsync() + { + var code = + """ + using Rocks; + using System; + using System.Collections.Generic; + + [assembly: RockMake(typeof(IWriter))] + + #nullable enable + + public interface IWriter + { + void WriteCollectionOfEnumValues(string? key, IEnumerable? values) where T : struct, Enum; + } + """; + + var generatedCode = + """ + // + + #pragma warning disable CS8775 + #nullable enable + + internal sealed class IWriterMakeExpectations + { + internal global::IWriter Instance() + { + return new Mock(); + } + + private sealed class Mock + : global::IWriter + { + public Mock() + { + } + + public void WriteCollectionOfEnumValues(string? @key, global::System.Collections.Generic.IEnumerable? @values) + where T : struct, global::System.Enum + { + } + } + } + + #pragma warning restore CS8775 + + """; + + await TestAssistants.RunGeneratorAsync(code, + [(typeof(RockAttributeGenerator), "IWriter_Rock_Make.g.cs", generatedCode)], + []); + } } \ No newline at end of file diff --git a/src/Rocks/Models/TypeReferenceModel.cs b/src/Rocks/Models/TypeReferenceModel.cs index bdd0a815..049e8301 100644 --- a/src/Rocks/Models/TypeReferenceModel.cs +++ b/src/Rocks/Models/TypeReferenceModel.cs @@ -29,8 +29,23 @@ internal TypeReferenceModel(ITypeSymbol type, Compilation compilation) { this.IsOpenGeneric = namedType.IsOpenGeneric(); this.Constraints = namedType.GetConstraints(compilation); - this.TypeArguments = namedType.TypeArguments.Select(_ => new TypeReferenceModel(_, compilation)).ToImmutableArray(); - this.TypeParameters = namedType.TypeParameters.Select(_ => new TypeReferenceModel(_, compilation)).ToImmutableArray(); + this.IsGenericType = namedType.IsGenericType; + + if (this.IsGenericType && !(this.TypeKind == TypeKind.TypeParameter)) + { + this.TypeArguments = namedType.TypeArguments.Select(_ => new TypeReferenceModel(_, compilation)).ToImmutableArray(); + this.TypeParameters = namedType.TypeParameters.Select(_ => new TypeReferenceModel(_, compilation)).ToImmutableArray(); + } + else + { + this.TypeArguments = ImmutableArray.Empty; + this.TypeParameters = ImmutableArray.Empty; + } + } + else + { + this.TypeArguments = ImmutableArray.Empty; + this.TypeParameters = ImmutableArray.Empty; } this.NullableAnnotation = type.NullableAnnotation; @@ -71,28 +86,50 @@ internal TypeReferenceModel(ITypeSymbol type, Compilation compilation) } } - private static string BuildName(TypeReferenceModel current, TypeArgumentsNamingContext parentNamingContext) => - !current.IsOpenGeneric ? + private static string BuildName(TypeReferenceModel current, TypeArgumentsNamingContext parentNamingContext) + { + static string GetNameForGeneric(TypeReferenceModel current, TypeArgumentsNamingContext parentNamingContext) + { + // We have to look at the current's type parameter count. If it's exactly 1, + // then if the current's FQN is essentially the "same" + // as the type parameter sans the nullable (think "T?"), then we add the type argument as-is. + // Otherwise we keep recursive descending. + // I don't like this heuristic, but I can't think of another way to handle this. + if (current.TypeParameters.Length == 1 && + current.FullyQualifiedName.Length == current.TypeParameters[0].FullyQualifiedName.Length + 1 && + current.FullyQualifiedName.StartsWith(current.TypeParameters[0].FullyQualifiedName)) + { + return current.FullyQualifiedName; + } + else + { + return $"{current.FullyQualifiedNameNoGenerics}<{string.Join(", ", current.TypeArguments.Select(_ => TypeReferenceModel.BuildName(_, parentNamingContext)))}>{(current.NullableAnnotation == NullableAnnotation.Annotated ? "?" : string.Empty)}"; + } + } + + return !current.IsOpenGeneric ? parentNamingContext[current.FullyQualifiedName] : current.IsTupleType ? $"({string.Join(", ", current.TypeArguments.Select(_ => TypeReferenceModel.BuildName(_, parentNamingContext)))})" : - $"{current.FullyQualifiedNameNoGenerics}<{string.Join(", ", current.TypeArguments.Select(_ => TypeReferenceModel.BuildName(_, parentNamingContext)))}>{(current.NullableAnnotation == NullableAnnotation.Annotated ? "?" : string.Empty)}"; + GetNameForGeneric(current, parentNamingContext); + } internal string BuildName(TypeArgumentsNamingContext parentNamingContext) => TypeReferenceModel.BuildName(this, parentNamingContext); - + internal string BuildName(TypeReferenceModel parent) => TypeReferenceModel.BuildName(this, new TypeArgumentsNamingContext(parent)); public override string ToString() => this.FullyQualifiedName; - internal string AttributesDescription { get; } + internal string AttributesDescription { get; } internal EquatableArray Constraints { get; } internal string FlattenedName { get; } internal string FullyQualifiedName { get; } internal string FullyQualifiedNameNoGenerics { get; } internal bool IsBasedOnTypeParameter { get; } internal bool IsEsoteric { get; } + internal bool IsGenericType { get; } internal bool IsOpenGeneric { get; } internal bool IsPointer { get; } internal bool IsRecord { get; }