Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 78 additions & 8 deletions Source/Mockerade.SourceGenerators/Entities/Class.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,54 @@ namespace Mockerade.SourceGenerators.Entities;

internal record Class
{
private string GetTypeName(ITypeSymbol type, List<string> 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<ITypeSymbol> 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<string> additionalNamespaces = [];
Namespace = type.ContainingNamespace.ToString();
ClassName = type.Name;
ClassName = GetTypeName(type, additionalNamespaces);

if (type.ContainingType is not null)
{
Expand All @@ -17,30 +61,53 @@ public Class(ITypeSymbol type)
}

IsInterface = type.TypeKind == TypeKind.Interface;
Methods = new EquatableArray<Method>(
type.GetMembers().OfType<IMethodSymbol>()
var y = GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType<IMethodSymbol>())
// 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<IMethodSymbol>())
// 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<Method>(methods.ToArray());
Properties = new EquatableArray<Property>(
type.GetMembers().OfType<IPropertySymbol>()
GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType<IPropertySymbol>())
.Where(x => !x.IsSealed)
.Where(x => IsInterface || x.IsVirtual || x.IsAbstract)
.Select(x => new Property(x))
.Distinct()
.ToArray());
Events = new EquatableArray<Event>(
type.GetMembers().OfType<IEventSymbol>()
GetBaseTypesAndThis(type).SelectMany(t => t.GetMembers().OfType<IEventSymbol>())
.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<string>(additionalNamespaces.Distinct().ToArray());
}

public Type? ContainingType { get; }

public EquatableArray<Method> Methods { get; }
public EquatableArray<string> AdditionalNamespaces { get; }

public EquatableArray<Property> Properties { get; }

Expand All @@ -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<string> EnumerateNamespaces()
Expand Down
8 changes: 5 additions & 3 deletions Source/Mockerade.SourceGenerators/Entities/Method.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
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)
{
Accessibility = methodSymbol.DeclaredAccessibility;
UseOverride = methodSymbol.IsVirtual || methodSymbol.IsAbstract;
ReturnType = methodSymbol.ReturnsVoid ? Type.Void : new Type(methodSymbol.ReturnType);
Name = methodSymbol.Name;
ContainingType = methodSymbol.ContainingType.Name;
Parameters = new EquatableArray<MethodParameter>(
methodSymbol.Parameters.Select(x => new MethodParameter(x)).ToArray());
}
Expand All @@ -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<MethodParameter> Parameters { get; }
public string? ExplicitImplementation { get; set; }
}
7 changes: 7 additions & 0 deletions Source/Mockerade.SourceGenerators/Entities/Property.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
2 changes: 1 addition & 1 deletion Source/Mockerade.SourceGenerators/Entities/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ internal static bool IsMockForInvocationExpressionSyntax(this SyntaxNode node)
{
Expression: MemberAccessExpressionSyntax
{
Expression: IdentifierNameSyntax { Identifier.Text: "Mock", },
Name: GenericNameSyntax { Identifier.Text : "For", }
}
};
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Method, bool>(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Method, bool>(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
? new Func<Method, bool>(e => e.ExplicitImplementation is null && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Method, bool>(e => e.ExplicitImplementation is null && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
var propertyPredicate = isProtected
? new Func<Property, bool>(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Property, bool>(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
? new Func<Property, bool>(e => !e.IsIndexer && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Property, bool>(e => !e.IsIndexer && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
if (!@class.Properties.Any(propertyPredicate) &&
!@class.Methods.Any(methodPredicate))
{
Expand All @@ -128,7 +128,7 @@ private static void AppendSetupExtensions(StringBuilder sb, Class @class, string
sb.Append("\t\t/// Setup for the property <see cref=\"").Append(@class.ClassName).Append(".").Append(property.Name).Append("\"/>.").AppendLine();
sb.Append("\t\t/// </summary>").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");
Expand Down Expand Up @@ -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<Method, bool>(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Method, bool>(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
? new Func<Method, bool>(e => e.ExplicitImplementation is null && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Method, bool>(e => e.ExplicitImplementation is null && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
if (!@class.Methods.Any(predicate))
{
return;
Expand Down Expand Up @@ -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<Property, bool>(e => e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Property, bool>(e => e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
? new Func<Property, bool>(e => !e.IsIndexer && e.Accessibility is Accessibility.Protected or Accessibility.ProtectedOrInternal)
: new Func<Property, bool>(e => !e.IsIndexer && e.Accessibility is not (Accessibility.Protected or Accessibility.ProtectedOrInternal));
if (!@class.Properties.Any(predicate))
{
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions Tests/Mockerade.SourceGenerators.Tests/Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>().That.Contains("<auto-generated>").And.Contains("</auto-generated>"),
});
}
[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<IList<int>>();
}
}
}
""");

await That(result.Diagnostics).IsEmpty();

await That(result.Sources)!.All().AreEquivalentTo(new
{
Source = It.Is<string>().That.Contains("<auto-generated>").And.Contains("</auto-generated>"),
Expand Down
Loading