diff --git a/changelog.md b/changelog.md index fd89b6e8..4e6486f1 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Static virtual members on interfaces will not be processed (issue [#337](https://github.com/JasonBock/Rocks/issues/3327)) - Special types (`ArgIterator`, `RuntimeArgumentHandle`, `TypedReference`) used as parameter or return types are now handled (issue [#329](https://github.com/JasonBock/Rocks/issues/329)) - Nested generic types are now handled correctly (issue [#333](https://github.com/JasonBock/Rocks/issues/333)) +- Members that hide other members are now handled correctly (issue [#336](https://github.com/JasonBock/Rocks/issues/336)) ## [8.3.1] - 2024-09-30 diff --git a/src/Rocks.Tests/Generators/InheritanceGeneratorTests.cs b/src/Rocks.Tests/Generators/InheritanceGeneratorTests.cs index fafe3389..4b1ce316 100644 --- a/src/Rocks.Tests/Generators/InheritanceGeneratorTests.cs +++ b/src/Rocks.Tests/Generators/InheritanceGeneratorTests.cs @@ -4,6 +4,303 @@ namespace Rocks.Tests.Generators; public static class InheritanceGeneratorTests { + [Test] + public static async Task GenerateWhenNewMethodIsIntroducedAsync() + { + var code = + """ + #nullable enable + + using Rocks; + using System; + + [assembly: Rock(typeof(BindableReactiveProperty<>), BuildType.Create | BuildType.Make)] + + public class ReactiveProperty + { + public virtual T RetrieveValue() => default!; + } + + public class BindableReactiveProperty + : ReactiveProperty + { + public new T RetrieveValue() => default!; + } + """; + + var createGeneratedCode = + """" + // + + #pragma warning disable CS8618 + #pragma warning disable CS8633 + #pragma warning disable CS8714 + #pragma warning disable CS8775 + + #nullable enable + + using Rocks.Extensions; + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class BindableReactivePropertyCreateExpectations + : global::Rocks.Expectations + { + internal sealed class Handler0 + : global::Rocks.Handler, bool> + { + public global::Rocks.Argument @obj { get; set; } + } + private global::Rocks.Handlers.Handler0>? @handlers0; + internal sealed class Handler1 + : global::Rocks.Handler, int> + { } + private global::Rocks.Handlers.Handler1>? @handlers1; + internal sealed class Handler2 + : global::Rocks.Handler, string?> + { } + private global::Rocks.Handlers.Handler2>? @handlers2; + + 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 (this.handlers1 is not null) { failures.AddRange(this.Verify(this.handlers1, 1)); } + if (this.handlers2 is not null) { failures.AddRange(this.Verify(this.handlers2, 2)); } + + if (failures.Count > 0) + { + throw new global::Rocks.Exceptions.VerificationException(failures); + } + } + } + + private sealed class Mock + : global::BindableReactiveProperty + { + public Mock(global::BindableReactivePropertyCreateExpectations @expectations) + { + this.Expectations = @expectations; + } + + [global::Rocks.MemberIdentifier(0)] + public override bool Equals(object? @obj) + { + if (this.Expectations.handlers0 is not null) + { + foreach (var @handler in this.Expectations.handlers0) + { + if (@handler.@obj.IsValid(@obj!)) + { + @handler.CallCount++; + var @result = @handler.Callback is not null ? + @handler.Callback(@obj!) : @handler.ReturnValue; + return @result!; + } + } + + throw new global::Rocks.Exceptions.ExpectationException( + $""" + No handlers match for {this.GetType().GetMemberDescription(0)} + obj: {@obj.FormatValue()} + """); + } + else + { + return base.Equals(@obj: @obj!); + } + } + + [global::Rocks.MemberIdentifier(1)] + public override int GetHashCode() + { + if (this.Expectations.handlers1 is not null) + { + var @handler = this.Expectations.handlers1.First; + @handler.CallCount++; + var @result = @handler.Callback is not null ? + @handler.Callback() : @handler.ReturnValue; + return @result!; + } + else + { + return base.GetHashCode(); + } + } + + [global::Rocks.MemberIdentifier(2)] + public override string? ToString() + { + if (this.Expectations.handlers2 is not null) + { + var @handler = this.Expectations.handlers2.First; + @handler.CallCount++; + var @result = @handler.Callback is not null ? + @handler.Callback() : @handler.ReturnValue; + return @result!; + } + else + { + return base.ToString(); + } + } + + private global::BindableReactivePropertyCreateExpectations Expectations { get; } + } + + internal sealed class MethodExpectations + { + internal MethodExpectations(global::BindableReactivePropertyCreateExpectations expectations) => + this.Expectations = expectations; + + internal global::BindableReactivePropertyCreateExpectations.Adornments.AdornmentsForHandler0 Equals(global::Rocks.Argument @obj) + { + global::Rocks.Exceptions.ExpectationException.ThrowIf(this.Expectations.WasInstanceInvoked); + global::System.ArgumentNullException.ThrowIfNull(@obj); + + var @handler = new global::BindableReactivePropertyCreateExpectations.Handler0 + { + @obj = @obj, + }; + + if (this.Expectations.handlers0 is null) { this.Expectations.handlers0 = new(@handler); } + else { this.Expectations.handlers0.Add(@handler); } + return new(@handler); + } + + internal new global::BindableReactivePropertyCreateExpectations.Adornments.AdornmentsForHandler1 GetHashCode() + { + global::Rocks.Exceptions.ExpectationException.ThrowIf(this.Expectations.WasInstanceInvoked); + var handler = new global::BindableReactivePropertyCreateExpectations.Handler1(); + if (this.Expectations.handlers1 is null) { this.Expectations.handlers1 = new(handler); } + else { this.Expectations.handlers1.Add(handler); } + return new(handler); + } + + internal new global::BindableReactivePropertyCreateExpectations.Adornments.AdornmentsForHandler2 ToString() + { + global::Rocks.Exceptions.ExpectationException.ThrowIf(this.Expectations.WasInstanceInvoked); + var handler = new global::BindableReactivePropertyCreateExpectations.Handler2(); + if (this.Expectations.handlers2 is null) { this.Expectations.handlers2 = new(handler); } + else { this.Expectations.handlers2.Add(handler); } + return new(handler); + } + + private global::BindableReactivePropertyCreateExpectations Expectations { get; } + } + + internal global::BindableReactivePropertyCreateExpectations.MethodExpectations Methods { get; } + + internal BindableReactivePropertyCreateExpectations() => + (this.Methods) = (new(this)); + + internal global::BindableReactiveProperty 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 IAdornmentsForBindableReactiveProperty + : global::Rocks.IAdornments + where TAdornments : IAdornmentsForBindableReactiveProperty + { } + + public sealed class AdornmentsForHandler0 + : global::Rocks.Adornments.Handler0, global::System.Func, bool>, IAdornmentsForBindableReactiveProperty + { + public AdornmentsForHandler0(global::BindableReactivePropertyCreateExpectations.Handler0 handler) + : base(handler) { } + } + public sealed class AdornmentsForHandler1 + : global::Rocks.Adornments.Handler1, global::System.Func, int>, IAdornmentsForBindableReactiveProperty + { + public AdornmentsForHandler1(global::BindableReactivePropertyCreateExpectations.Handler1 handler) + : base(handler) { } + } + public sealed class AdornmentsForHandler2 + : global::Rocks.Adornments.Handler2, global::System.Func, string?>, IAdornmentsForBindableReactiveProperty + { + public AdornmentsForHandler2(global::BindableReactivePropertyCreateExpectations.Handler2 handler) + : base(handler) { } + } + } + } + + #pragma warning restore CS8618 + #pragma warning restore CS8633 + #pragma warning restore CS8714 + #pragma warning restore CS8775 + """"; + + var makeGeneratedCode = + """ + // + + #pragma warning disable CS8618 + #pragma warning disable CS8633 + #pragma warning disable CS8714 + #pragma warning disable CS8775 + + #nullable enable + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class BindableReactivePropertyMakeExpectations + { + internal global::BindableReactiveProperty Instance() + { + return new Mock(); + } + + private sealed class Mock + : global::BindableReactiveProperty + { + public Mock() + { + } + + public override bool Equals(object? @obj) + { + return default!; + } + public override int GetHashCode() + { + return default!; + } + public override string? ToString() + { + return default!; + } + } + } + + #pragma warning restore CS8618 + #pragma warning restore CS8633 + #pragma warning restore CS8714 + #pragma warning restore CS8775 + + """; + + await TestAssistants.RunGeneratorAsync(code, + [ + ("BindableReactivePropertyT_Rock_Create.g.cs", createGeneratedCode), + ("BindableReactivePropertyT_Rock_Make.g.cs", makeGeneratedCode) + ], + []); + } + [Test] public static async Task GenerateWhenNewPropertyIsIntroducedAsync() { diff --git a/src/Rocks/Discovery/MockableMethodDiscovery.cs b/src/Rocks/Discovery/MockableMethodDiscovery.cs index 1330ab94..0bde309c 100644 --- a/src/Rocks/Discovery/MockableMethodDiscovery.cs +++ b/src/Rocks/Discovery/MockableMethodDiscovery.cs @@ -51,7 +51,9 @@ private static MockableMethods GetMethodsForClass(ITypeSymbol mockType, IAssembl foreach (var hierarchyMethod in hierarchyMethods) { - if (hierarchyMethod.IsStatic && hierarchyMethod.CanBeSeenByContainingAssembly(containingAssemblyOfInvocationSymbol)) + var canBeSeen = hierarchyMethod.CanBeSeenByContainingAssembly(containingAssemblyOfInvocationSymbol); + + if (canBeSeen) { // This is the case where a class does something like this: // `protected static new string ToString()` @@ -67,27 +69,18 @@ private static MockableMethods GetMethodsForClass(ITypeSymbol mockType, IAssembl methods.Remove(methodToRemove); } } - else if (!hierarchyMethod.IsStatic && (!mockType.IsRecord || hierarchyMethod.Name != nameof(Equals))) + + if (!hierarchyMethod.IsStatic && (!mockType.IsRecord || hierarchyMethod.Name != nameof(Equals))) { if (hierarchyMethod.IsAbstract || hierarchyMethod.IsOverride || hierarchyMethod.IsVirtual) { - var canBeSeen = hierarchyMethod.CanBeSeenByContainingAssembly(containingAssemblyOfInvocationSymbol); - if (!canBeSeen && hierarchyMethod.IsAbstract) { inaccessibleAbstractMembers = true; } else if (canBeSeen) { - var methodToRemove = methods.SingleOrDefault(_ => !(_.Value.Match(hierarchyMethod) == MethodMatch.None)); - - if (methodToRemove is not null) - { - methods.Remove(methodToRemove); - } - - if ((methodToRemove is null || !methodToRemove.Value.ContainingType.Equals(hierarchyMethod.ContainingType)) && - !hierarchyMethod.IsSealed) + if (!hierarchyMethod.IsSealed) { methods.Add(new(hierarchyMethod, mockType, RequiresExplicitInterfaceImplementation.No, RequiresOverride.Yes, objectMethods.Any(_ => hierarchyMethod.Name == _.Name && hierarchyMethod.Parameters.Length == 0) ? diff --git a/src/Rocks/Discovery/MockablePropertyDiscovery.cs b/src/Rocks/Discovery/MockablePropertyDiscovery.cs index 6d9a1858..faa09ca6 100644 --- a/src/Rocks/Discovery/MockablePropertyDiscovery.cs +++ b/src/Rocks/Discovery/MockablePropertyDiscovery.cs @@ -27,7 +27,7 @@ static bool AreParametersEqual(IPropertySymbol property1, IPropertySymbol proper var property1Parameter = property1.Parameters[i]; var property2Parameter = property2.Parameters[i]; - if (!property1Parameter.Type.Equals(property2Parameter.Type)) + if (!SymbolEqualityComparer.Default.Equals(property1Parameter.Type, property2Parameter.Type)) { return false; }