Skip to content

Commit

Permalink
Refactor into Invocation > Parser > Emitter
Browse files Browse the repository at this point in the history
  • Loading branch information
justindbaur committed Nov 26, 2023
1 parent 8528f81 commit d01a3a1
Show file tree
Hide file tree
Showing 20 changed files with 774 additions and 448 deletions.
59 changes: 59 additions & 0 deletions src/Pretender.SourceGenerator/Emitter/SetupEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using Microsoft.CodeAnalysis.Operations;

namespace Pretender.SourceGenerator.Emitter
{
internal class SetupEmitter
{
private readonly SetupActionEmitter _setupActionEmitter;
private readonly IInvocationOperation _setupInvocation;

public SetupEmitter(SetupActionEmitter setupActionEmitter, IInvocationOperation setupInvocation)
{
_setupActionEmitter = setupActionEmitter;
_setupInvocation = setupInvocation;
}

// TODO: Run cancellationToken a lot more
public MemberDeclarationSyntax[] Emit(int index, CancellationToken cancellationToken)
{
var setupMethod = _setupActionEmitter.SetupMethod;
var pretendType = _setupActionEmitter.PretendType;

var allMembers = new List<MemberDeclarationSyntax>();

var interceptsLocation = new InterceptsLocationInfo(_setupInvocation);

// TODO: This is wrong
var typeArguments = setupMethod.ReturnsVoid
? TypeArgumentList(SingletonSeparatedList(ParseTypeName(pretendType.ToFullDisplayString())))
: TypeArgumentList(SeparatedList([ParseTypeName(pretendType.ToFullDisplayString()), setupMethod.ReturnType.AsUnknownTypeSyntax()]));

var returnType = GenericName("IPretendSetup")
.WithTypeArgumentList(typeArguments);

var setupCreatorInvocation = _setupActionEmitter.CreateSetupGetter(cancellationToken);

var fullSetupMethod = MethodDeclaration(returnType, $"Setup{index}")
.WithBody(Block(ReturnStatement(setupCreatorInvocation)))
.WithParameterList(ParameterList(SeparatedList(new[]
{
Parameter(Identifier("pretend"))
.WithModifiers(TokenList(Token(SyntaxKind.ThisKeyword)))
.WithType(ParseTypeName($"Pretend<{pretendType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>")),

Parameter(Identifier("setupExpression"))
.WithType(GenericName(setupMethod.ReturnsVoid ? "Action" : "Func").WithTypeArgumentList(typeArguments)),
})))
.WithModifiers(TokenList(Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword)))
.WithAttributeLists(SingletonList(AttributeList(
SingletonSeparatedList(interceptsLocation.ToAttributeSyntax()))));

allMembers.Add(fullSetupMethod);
return [.. allMembers];
}
}
}
12 changes: 6 additions & 6 deletions src/Pretender.SourceGenerator/Emitter/VerifyEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ internal class VerifyEmitter
{
private readonly ITypeSymbol _pretendType;
private readonly ITypeSymbol? _returnType;
private readonly SetupCreationSpec _setupCreationSpec;
private readonly SetupActionEmitter _setupActionEmitter;
private readonly IInvocationOperation _invocationOperation;

public VerifyEmitter(ITypeSymbol pretendType, ITypeSymbol? returnType, SetupCreationSpec setupCreationSpec, IInvocationOperation invocationOperation)
public VerifyEmitter(ITypeSymbol pretendType, ITypeSymbol? returnType, SetupActionEmitter setupActionEmitter, IInvocationOperation invocationOperation)
{
_pretendType = pretendType;
_returnType = returnType;
_setupCreationSpec = setupCreationSpec;
_setupActionEmitter = setupActionEmitter;
_invocationOperation = invocationOperation;
}

public MethodDeclarationSyntax EmitVerifyMethod(int index, CancellationToken cancellationToken)
public MethodDeclarationSyntax Emit(int index, CancellationToken cancellationToken)
{
var setupGetter = _setupCreationSpec.CreateSetupGetter(cancellationToken);
var setupInvocation = _setupActionEmitter.CreateSetupGetter(cancellationToken);

// var setup = pretend.GetOrCreateSetup(...);
var setupLocal = LocalDeclarationStatement(VariableDeclaration(CommonSyntax.VarType)
.WithVariables(SingletonSeparatedList(VariableDeclarator(CommonSyntax.SetupIdentifier)
.WithInitializer(EqualsValueClause(setupGetter)))));
.WithInitializer(EqualsValueClause(setupInvocation)))));

TypeSyntax pretendType = ParseTypeName(_pretendType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));

Expand Down
35 changes: 0 additions & 35 deletions src/Pretender.SourceGenerator/InvocationOperationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System.Collections.Immutable;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Operations;

namespace Pretender.SourceGenerator
Expand All @@ -20,39 +18,6 @@ public static bool IsInvocationOperation(this IOperation? operation, out IInvoca
return false;
}

public static bool IsSetupCall(this SyntaxNode node)
{
return node is InvocationExpressionSyntax
{
Expression: MemberAccessExpressionSyntax
{
// pretend.Setup(i => i.Something());
Name.Identifier.ValueText: "Setup" or "SetupSet",
},
ArgumentList.Arguments.Count: 1
};
}

public static bool IsValidSetupOperation(this IOperation operation, Compilation compilation, out IInvocationOperation? invocation)
{
var pretendType = compilation.GetTypeByMetadataName("Pretender.Pretend`1");
invocation = null;

// TODO: Probably need to check a few more things
// Someone could make a Setup extension method, that doesn't look
// like I think it should, I need to check the return type and first arg type
// a lot more closely.
if (operation is IInvocationOperation targetOperation
&& targetOperation.Instance is not null
&& SymbolEqualityComparer.Default.Equals(targetOperation.Instance.Type!.OriginalDefinition, pretendType))
{
invocation = targetOperation;
return true;
}

return false;
}

public static bool IsValidCreateOperation(this IOperation? operation, Compilation compilation, out IInvocationOperation invocationOperation, out ImmutableArray<ITypeSymbol>? typeArguments)
{
var pretendGeneric = compilation.GetTypeByMetadataName("Pretender.Pretend`1");
Expand Down
14 changes: 14 additions & 0 deletions src/Pretender.SourceGenerator/Parser/KnownTypeSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,27 @@ internal sealed class KnownTypeSymbols
public INamedTypeSymbol? Pretend { get; }
public INamedTypeSymbol? Pretend_Unbound { get; }

public INamedTypeSymbol? Task { get; }
public INamedTypeSymbol? TaskOfT { get; }
public INamedTypeSymbol? ValueTask { get; }
public INamedTypeSymbol? ValueTaskOfT { get; }



public KnownTypeSymbols(CSharpCompilation compilation)
{
Compilation = compilation;

// TODO: Get known types
Pretend = compilation.GetTypeByMetadataName("Pretender.Pretend`1");
Pretend_Unbound = Pretend?.ConstructUnboundGenericType();

Task = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");
// TODO: Create unbounded?
TaskOfT = compilation.GetTypeByMetadataName("System.Threading.Tasks.Task`1");
ValueTask = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask");
// TODO: Create unbounded?
ValueTaskOfT = compilation.GetTypeByMetadataName("System.Threading.Tasks.ValueTask`1");
}

public static bool IsPretend(INamedTypeSymbol type)
Expand Down
177 changes: 177 additions & 0 deletions src/Pretender.SourceGenerator/Parser/SetupActionParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System.Collections.Immutable;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Pretender.SourceGenerator.SetupArguments;

namespace Pretender.SourceGenerator.Parser
{
internal class SetupActionParser
{
private readonly IOperation _setupActionArgument;
private readonly ITypeSymbol _pretendType;
private readonly bool _forcePropertySetter;

// TODO: Should I have a higher IOperation kind here? Like InvocationOperation?
public SetupActionParser(IOperation setupActionArgument, ITypeSymbol pretendType, bool forcePropertySetter)
{
_setupActionArgument = setupActionArgument;
_pretendType = pretendType;
_forcePropertySetter = forcePropertySetter;
}

public (SetupActionEmitter? Emitter, ImmutableArray<Diagnostic>? Diagnostics) Parse(CancellationToken cancellationToken)
{
var candidates = GetInvocationCandidates(cancellationToken);

if (candidates.Length == 0)
{
// TODO: Create error diagnostic
return (null, null);
}
else if (candidates.Length != 1)
{
// TODO: Create error diagnostic
return (null, null);
}

var candidate = candidates[0];

var arguments = candidate.Arguments;

var builder = ImmutableArray.CreateBuilder<SetupArgumentSpec>(arguments.Length);
for (var i = 0; i < arguments.Length; i++)
{
builder.Add(SetupArgumentSpec.Create(arguments[i], i));
}

return (new SetupActionEmitter(_pretendType, candidate.Method, builder.MoveToImmutable()), null);
}

private ImmutableArray<InvocationCandidate> GetInvocationCandidates(CancellationToken cancellationToken)
{
var builder = ImmutableArray.CreateBuilder<InvocationCandidate>();
TraverseOperation(_setupActionArgument, builder, cancellationToken);
return builder.ToImmutable();
}

private void TraverseOperation(IOperation operation, ImmutableArray<InvocationCandidate>.Builder invocationCandidates, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

switch (operation.Kind)
{
case OperationKind.Block:
var blockOperation = (IBlockOperation)operation;
TraverseOperationList(blockOperation.Operations, invocationCandidates, cancellationToken);
break;
case OperationKind.Return:
var returnOperation = (IReturnOperation)operation;
if (returnOperation.ReturnedValue != null)
{
TraverseOperation(returnOperation.ReturnedValue, invocationCandidates, cancellationToken);
}
break;
case OperationKind.ExpressionStatement:
var expressionStatement = (IExpressionStatementOperation)operation;
TraverseOperation(expressionStatement.Operation, invocationCandidates, cancellationToken);
break;
case OperationKind.Conversion:
var conversionOperation = (IConversionOperation)operation;
TraverseOperation(conversionOperation.Operand, invocationCandidates, cancellationToken);
break;
case OperationKind.Invocation:
var invocationOperation = (IInvocationOperation)operation;
TryMatchInvocationOperation(invocationOperation, invocationCandidates);
break;
case OperationKind.PropertyReference:
var propertyReferenceOperation = (IPropertyReferenceOperation)operation;
TryMatchPropertyReference(propertyReferenceOperation, invocationCandidates);
break;
case OperationKind.AnonymousFunction:
var anonymousFunctionOperation = (IAnonymousFunctionOperation)operation;
TraverseOperation(anonymousFunctionOperation.Body, invocationCandidates, cancellationToken);
break;
case OperationKind.DelegateCreation:
var delegateCreationOperation = (IDelegateCreationOperation)operation;
TraverseOperation(delegateCreationOperation.Target, invocationCandidates, cancellationToken);
break;
default:
#if DEBUG
// TODO: Figure out what operation caused this, it's not ideal to "randomly" support operations
Debugger.Launch();
#endif
// Absolute fallback, most of our operations can be supported this way but it's nicer to be explicit
TraverseOperationList(operation.ChildOperations, invocationCandidates, cancellationToken);
break;
}
}

private void TraverseOperationList(IEnumerable<IOperation> operations, ImmutableArray<InvocationCandidate>.Builder invocationCandidates, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
foreach (var operation in operations)
{
cancellationToken.ThrowIfCancellationRequested();
TraverseOperation(operation, invocationCandidates, cancellationToken);
}
}

private void TryMatchPropertyReference(IPropertyReferenceOperation propertyReference, ImmutableArray<InvocationCandidate>.Builder invocationCandidates)
{
if (propertyReference.Instance is not IParameterReferenceOperation parameterReference)
{
return;
}

if (!SymbolEqualityComparer.Default.Equals(parameterReference.Type, _pretendType))
{
return;
}

var method = _forcePropertySetter
? propertyReference.Property.SetMethod
: propertyReference.Property.GetMethod;

if (method == null)
{
return;
}

invocationCandidates.Add(new InvocationCandidate(method, ImmutableArray<IArgumentOperation>.Empty));
}

private void TryMatchInvocationOperation(IInvocationOperation invocation, ImmutableArray<InvocationCandidate>.Builder invocationCandidates)
{
if (_forcePropertySetter)
{
return;
}

if (invocation.Instance is not IParameterReferenceOperation parameterReference)
{
return;
}

if (!SymbolEqualityComparer.Default.Equals(parameterReference.Type, _pretendType))
{
return;
}

// TODO: Any more validation?
invocationCandidates.Add(new InvocationCandidate(invocation.TargetMethod, invocation.Arguments));
}

private class InvocationCandidate
{
public InvocationCandidate(IMethodSymbol methodSymbol, ImmutableArray<IArgumentOperation> argumentOperations)
{
Method = methodSymbol;
Arguments = argumentOperations;
}

public IMethodSymbol Method { get; }
public ImmutableArray<IArgumentOperation> Arguments { get; }
}
}
}
Loading

0 comments on commit d01a3a1

Please sign in to comment.