diff --git a/Source/Mockerade.SourceGenerators/Entities/Class.cs b/Source/Mockerade.SourceGenerators/Entities/Class.cs index 4ff3e82..689b175 100644 --- a/Source/Mockerade.SourceGenerators/Entities/Class.cs +++ b/Source/Mockerade.SourceGenerators/Entities/Class.cs @@ -5,10 +5,54 @@ namespace Mockerade.SourceGenerators.Entities; internal record Class { + private string GetTypeName(ITypeSymbol type, List additionalNamespaces) + { + if (type is INamedTypeSymbol namedType) + { + if (namedType.IsGenericType) + { + additionalNamespaces.AddRange(namedType.TypeArguments + .Select(t => t.ContainingNamespace.ToString())); + return namedType.Name + "<" + string.Join(",", namedType.TypeArguments.Select(t => GetTypeName(t, additionalNamespaces))) + ">"; + } + return namedType.SpecialType switch + { + SpecialType.System_Int32 => "int", + SpecialType.System_Int64 => "long", + SpecialType.System_Int16 => "short", + SpecialType.System_UInt32 => "uint", + SpecialType.System_UInt64 => "ulong", + SpecialType.System_UInt16 => "ushort", + _ => type.Name + }; + } + else + { + return type.Name; + } + } + public static IEnumerable GetBaseTypesAndThis(ITypeSymbol type) + { + var current = type; + while (current != null) + { + yield return current; + if (current.TypeKind == TypeKind.Interface) + { + foreach (var @interface in current.Interfaces) + { + yield return @interface; + } + } + current = current.BaseType; + } + } + public Class(ITypeSymbol type) { + List additionalNamespaces = []; Namespace = type.ContainingNamespace.ToString(); - ClassName = type.Name; + ClassName = GetTypeName(type, additionalNamespaces); if (type.ContainingType is not null) { @@ -17,30 +61,53 @@ public Class(ITypeSymbol type) } IsInterface = type.TypeKind == TypeKind.Interface; - Methods = new EquatableArray( - type.GetMembers().OfType() + var y = GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType()) + // Exclude getter/setter methods + .Where(x => x.AssociatedSymbol is null && !x.IsSealed) + .Where(x => IsInterface || x.IsVirtual || x.IsAbstract).ToList(); + var methods = GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType()) // Exclude getter/setter methods - .Where(x => x.AssociatedSymbol is null) + .Where(x => x.AssociatedSymbol is null && !x.IsSealed) .Where(x => IsInterface || x.IsVirtual || x.IsAbstract) .Select(x => new Method(x)) - .ToArray()); + .Distinct() + .ToList(); + for (int i = 0; i < methods.Count; i++) + { + var method = methods[i]; + if (methods.Take(i) + .Any(m => + m.Name == method.Name && + m.Parameters.Count == method.Parameters.Count && + m.Parameters.SequenceEqual(method.Parameters))) + { + methods[i] = method with { ExplicitImplementation = method.ContainingType }; + } + } + Methods = new EquatableArray(methods.ToArray()); Properties = new EquatableArray( - type.GetMembers().OfType() + GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType()) + .Where(x => !x.IsSealed) .Where(x => IsInterface || x.IsVirtual || x.IsAbstract) .Select(x => new Property(x)) + .Distinct() .ToArray()); Events = new EquatableArray( - type.GetMembers().OfType() + GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType()) + .Where(x => !x.IsSealed) .Where(x => IsInterface || x.IsVirtual || x.IsAbstract) .Select(x => (x, (x.Type as INamedTypeSymbol)?.DelegateInvokeMethod)) .Where(x => x.DelegateInvokeMethod is not null) .Select(x => new Event(x.x, x.DelegateInvokeMethod!)) + .Distinct() .ToArray()); + AdditionalNamespaces = new EquatableArray(additionalNamespaces.Distinct().ToArray()); } public Type? ContainingType { get; } public EquatableArray Methods { get; } + public EquatableArray AdditionalNamespaces { get; } public EquatableArray Properties { get; } @@ -51,7 +118,10 @@ public Class(ITypeSymbol type) public string ClassName { get; } public string GetClassNameWithoutDots() - => ClassName.Replace(".", ""); + => ClassName + .Replace(".", "") + .Replace("<", "") + .Replace(">", ""); public string[] GetClassNamespaces() => EnumerateNamespaces().Distinct().OrderBy(n => n).ToArray(); internal IEnumerable EnumerateNamespaces() diff --git a/Source/Mockerade.SourceGenerators/Entities/Method.cs b/Source/Mockerade.SourceGenerators/Entities/Method.cs index 09f41c9..b721471 100644 --- a/Source/Mockerade.SourceGenerators/Entities/Method.cs +++ b/Source/Mockerade.SourceGenerators/Entities/Method.cs @@ -1,10 +1,9 @@ -using System.Text; -using Mockerade.SourceGenerators.Internals; +using Mockerade.SourceGenerators.Internals; using Microsoft.CodeAnalysis; namespace Mockerade.SourceGenerators.Entities; -internal readonly record struct Method +internal record struct Method { public Method(IMethodSymbol methodSymbol) { @@ -12,6 +11,7 @@ public Method(IMethodSymbol methodSymbol) UseOverride = methodSymbol.IsVirtual || methodSymbol.IsAbstract; ReturnType = methodSymbol.ReturnsVoid ? Type.Void : new Type(methodSymbol.ReturnType); Name = methodSymbol.Name; + ContainingType = methodSymbol.ContainingType.Name; Parameters = new EquatableArray( methodSymbol.Parameters.Select(x => new MethodParameter(x)).ToArray()); } @@ -21,5 +21,7 @@ public Method(IMethodSymbol methodSymbol) public Accessibility Accessibility { get; } public Type ReturnType { get; } public string Name { get; } + public string ContainingType { get; } public EquatableArray Parameters { get; } + public string? ExplicitImplementation { get; set; } } diff --git a/Source/Mockerade.SourceGenerators/Entities/Property.cs b/Source/Mockerade.SourceGenerators/Entities/Property.cs index 32210e7..2e4ad1c 100644 --- a/Source/Mockerade.SourceGenerators/Entities/Property.cs +++ b/Source/Mockerade.SourceGenerators/Entities/Property.cs @@ -12,10 +12,17 @@ public Property(IPropertySymbol propertySymbol) UseOverride = propertySymbol.IsVirtual || propertySymbol.IsAbstract; Name = propertySymbol.Name; Type = new Type(propertySymbol.Type); + IsIndexer = propertySymbol.IsIndexer; + if (IsIndexer && propertySymbol.Parameters.Length > 0) + { + IndexerParameter = new MethodParameter(propertySymbol.Parameters[0]); + } Getter = propertySymbol.GetMethod is null ? null : new Method(propertySymbol.GetMethod); Setter = propertySymbol.SetMethod is null ? null : new Method(propertySymbol.SetMethod); } + public bool IsIndexer { get; } + public MethodParameter? IndexerParameter { get; } public Type Type { get; } public Method? Setter { get; } diff --git a/Source/Mockerade.SourceGenerators/Entities/Type.cs b/Source/Mockerade.SourceGenerators/Entities/Type.cs index 29d2e39..722d35c 100644 --- a/Source/Mockerade.SourceGenerators/Entities/Type.cs +++ b/Source/Mockerade.SourceGenerators/Entities/Type.cs @@ -12,7 +12,7 @@ private Type(string fullname) internal Type(ITypeSymbol typeSymbol) { Fullname = typeSymbol.ToDisplayString(); - Namespace = typeSymbol.ContainingNamespace.ToString(); + Namespace = typeSymbol.ContainingNamespace?.ToString(); } public string? Namespace { get; } diff --git a/Source/Mockerade.SourceGenerators/Internals/GeneratorHelpers.cs b/Source/Mockerade.SourceGenerators/Internals/GeneratorHelpers.cs index 83eadd9..ef0b198 100644 --- a/Source/Mockerade.SourceGenerators/Internals/GeneratorHelpers.cs +++ b/Source/Mockerade.SourceGenerators/Internals/GeneratorHelpers.cs @@ -11,7 +11,6 @@ internal static bool IsMockForInvocationExpressionSyntax(this SyntaxNode node) { Expression: MemberAccessExpressionSyntax { - Expression: IdentifierNameSyntax { Identifier.Text: "Mock", }, Name: GenericNameSyntax { Identifier.Text : "For", } } }; @@ -26,7 +25,8 @@ internal static bool TryExtractGenericNameSyntax(this SyntaxNode syntaxNode, ISymbol? symbol = semanticModel.GetSymbolInfo(syntaxNode).Symbol; genericNameSyntax = value; return symbol?.ContainingType.ContainingNamespace.ContainingNamespace.IsGlobalNamespace == true && - symbol.ContainingType.ContainingNamespace.Name == "Mockerade"; + symbol.ContainingType.ContainingNamespace.Name == "Mockerade" && + symbol.ContainingType.Name == "Mock"; } genericNameSyntax = null; diff --git a/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.ExtensionsClass.cs b/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.ExtensionsClass.cs index 185a5fe..07c7e1e 100644 --- a/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.ExtensionsClass.cs +++ b/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.ExtensionsClass.cs @@ -104,11 +104,11 @@ private static void AppendRaisesExtensions(StringBuilder sb, Class @class, strin private static void AppendSetupExtensions(StringBuilder sb, Class @class, string[] namespaces, bool isProtected = false) { var methodPredicate = isProtected - ? new Func(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) - : new Func(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); + ? new Func(e => e.ExplicitImplementation is null && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) + : new Func(e => e.ExplicitImplementation is null && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); var propertyPredicate = isProtected - ? new Func(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) - : new Func(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); + ? new Func(e => !e.IsIndexer && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) + : new Func(e => !e.IsIndexer && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); if (!@class.Properties.Any(propertyPredicate) && !@class.Methods.Any(methodPredicate)) { @@ -128,7 +128,7 @@ private static void AppendSetupExtensions(StringBuilder sb, Class @class, string sb.Append("\t\t/// Setup for the property .").AppendLine(); sb.Append("\t\t/// ").AppendLine(); sb.Append("\t\tpublic PropertySetup<").Append(property.Type.GetMinimizedString(namespaces)).Append("> ") - .Append(property.Name).AppendLine(); + .Append((property.IndexerParameter is not null ? property.Name.Replace("[]", $"[With.Parameter<{property.IndexerParameter.Value.Type.GetMinimizedString(namespaces)}> {property.IndexerParameter.Value.Name}]") : property.Name)).AppendLine(); sb.AppendLine("\t\t{"); sb.AppendLine("\t\t\tget"); @@ -260,8 +260,8 @@ private static void AppendSetupExtensions(StringBuilder sb, Class @class, string private static void AppendInvokedExtensions(StringBuilder sb, Class @class, string[] namespaces, bool isProtected = false) { var predicate = isProtected - ? new Func(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) - : new Func(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); + ? new Func(e => e.ExplicitImplementation is null && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) + : new Func(e => e.ExplicitImplementation is null && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); if (!@class.Methods.Any(predicate)) { return; @@ -313,8 +313,8 @@ private static void AppendInvokedExtensions(StringBuilder sb, Class @class, stri private static void AppendAccessedExtensions(StringBuilder sb, Class @class, string[] namespaces, bool isProtected = false) { var predicate = isProtected - ? new Func(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) - : new Func(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); + ? new Func(e => !e.IsIndexer && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal) + : new Func(e => !e.IsIndexer && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal)); if (!@class.Properties.Any(predicate)) { return; diff --git a/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.MockClass.cs b/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.MockClass.cs index 89e5137..1a79453 100644 --- a/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.MockClass.cs +++ b/Source/Mockerade.SourceGenerators/Internals/SourceGeneration.MockClass.cs @@ -241,7 +241,7 @@ private static void ImplementClass(StringBuilder sb, Class @class, string[] name sb.Append("override "); } sb.Append(property.Type.GetMinimizedString(namespaces)) - .Append(" ").Append(property.Name).AppendLine(); + .Append(" ").Append((property.IndexerParameter is not null ? property.Name.Replace("[]", $"[{property.IndexerParameter.Value.Type.GetMinimizedString(namespaces)} {property.IndexerParameter.Value.Name}]") : property.Name)).AppendLine(); } sb.AppendLine("\t\t{"); if (property.Getter != null && property.Getter.Value.Accessibility != Microsoft.CodeAnalysis.Accessibility.Private) @@ -292,13 +292,22 @@ private static void ImplementClass(StringBuilder sb, Class @class, string[] name } else { - sb.Append("\t\t").Append(method.Accessibility.ToVisibilityString()).Append(' '); - if (!@class.IsInterface && method.UseOverride) + sb.Append("\t\t"); + if (method.ExplicitImplementation is null) { - sb.Append("override "); + sb.Append(method.Accessibility.ToVisibilityString()).Append(' '); + if (!@class.IsInterface && method.UseOverride) + { + sb.Append("override "); + } + sb.Append(method.ReturnType.GetMinimizedString(namespaces)).Append(' ') + .Append(method.Name).Append('('); + } + else + { + sb.Append(method.ReturnType.GetMinimizedString(namespaces)).Append(' ') + .Append(method.ExplicitImplementation).Append('.').Append(method.Name).Append('('); } - sb.Append(method.ReturnType.GetMinimizedString(namespaces)).Append(' ') - .Append(method.Name).Append('('); } int index = 0; foreach (MethodParameter parameter in method.Parameters) diff --git a/Tests/Mockerade.SourceGenerators.Tests/Test.cs b/Tests/Mockerade.SourceGenerators.Tests/Test.cs index 159eae3..53f44ab 100644 --- a/Tests/Mockerade.SourceGenerators.Tests/Test.cs +++ b/Tests/Mockerade.SourceGenerators.Tests/Test.cs @@ -21,6 +21,32 @@ public static void Main(string[] args) await That(result.Diagnostics).IsEmpty(); + await That(result.Sources)!.All().AreEquivalentTo(new + { + Source = It.Is().That.Contains("").And.Contains(""), + }); + } + [Fact] + public async Task XXX() + { + GeneratorResult result = Generator + .Run(""" + using System.Collections.Generic; + + namespace MyCode + { + public class Program + { + public static void Main(string[] args) + { + var x = Mockerade.Mock.For>(); + } + } + } + """); + + await That(result.Diagnostics).IsEmpty(); + await That(result.Sources)!.All().AreEquivalentTo(new { Source = It.Is().That.Contains("").And.Contains(""),