Skip to content

Commit

Permalink
Feature Enable asyncronous event observables to be generated (#141)
Browse files Browse the repository at this point in the history
* Enable BlockSyntax in LocalFunctionStatement

* enable events with Task and ValueTask as return type

* Add generation of asyncronous handlers

* adding test class for asyncronous events
  • Loading branch information
nilsauf authored Sep 5, 2022
1 parent 556eb20 commit 5780647
Show file tree
Hide file tree
Showing 8 changed files with 65 additions and 35 deletions.
16 changes: 13 additions & 3 deletions src/ReactiveMarbles.ObservableEvents.Example/MyForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
// See the LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Reactive;
using System.Text;
using System.Threading.Tasks;

using ReactiveMarbles.ObservableEvents;

Expand Down Expand Up @@ -40,6 +38,18 @@ public MyNoEventsClass()
}
}

public class AsyncEventsClass
{
public event Func<int, Task> TestEvent1;
public event Func<int, ValueTask> TestEvent2;

public AsyncEventsClass()
{
var e1 = this.Events().TestEvent1;
var e2 = this.Events().TestEvent2;
}
}

public static class StaticTest
{
public static event EventHandler? TestChanged;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ private static bool GenerateEvents(

processedItems.Add(item);

var namespaceItem = symbolGenerator.Generate(item);
var namespaceItem = symbolGenerator.Generate(item, context.Compilation.GetTypeByMetadataName);

if (namespaceItem == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -20,7 +20,7 @@ namespace ReactiveMarbles.ObservableEvents.SourceGenerator.EventGenerators.Gener
internal abstract class EventGeneratorBase : IEventSymbolGenerator
{
/// <inheritdoc />
public abstract NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item);
public abstract NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item, Func<string, ITypeSymbol?> getSymbolOf);

/// <summary>
/// Generates an observable declaration that wraps a event.
Expand Down Expand Up @@ -66,8 +66,6 @@ private static (ArrowExpressionClauseSyntax ArrowClause, TypeSyntax EventArgsTyp
return default;
}

var returnType = IdentifierName(eventSymbol.Type.GenerateFullGenericName());

// If we are using a standard approach of using 2 parameters only send the "Value", not the sender.
if (invokeMethod.Parameters.Length == 2 && invokeMethod.Parameters[0].Type.GenerateFullGenericName() == "object")
{
Expand All @@ -91,14 +89,11 @@ private static (ArrowExpressionClauseSyntax ArrowClause, TypeSyntax EventArgsTyp

var eventName = eventSymbol.Name;

var localFunctionExpression = GenerateLocalHandler(methodParametersArgumentList, invokeMethod, "obs.OnNext");
var localFunctionExpression = GenerateLocalHandler(methodParametersArgumentList, invokeMethod, "obs.OnNext", invokeMethod.ReturnType);

////// Produces lambda expression: eventHandler => (local function above); return Handler;
////var conversionLambdaExpression = SimpleLambdaExpression(Parameter("eventHandler"), Block(new StatementSyntax[] { localFunctionExpression, ReturnStatement("Handler") }, 3));

// Produces type parameters: <EventArg1Type, EventArg2Type>
var fromEventTypeParameters = new[] { returnType, eventArgsType };

var conversionLambdaExpression = SimpleLambdaExpression(Parameter("obs"), Block(
new StatementSyntax[]
{
Expand Down Expand Up @@ -129,15 +124,42 @@ private static (ArrowExpressionClauseSyntax ArrowClause, TypeSyntax EventArgsTyp
return (expression, eventArgsType.GenerateObservableType());
}

private static LocalFunctionStatementSyntax GenerateLocalHandler(IReadOnlyCollection<ArgumentSyntax> methodParametersArgumentList, IMethodSymbol invokeMethod, string handlerName) =>
private static LocalFunctionStatementSyntax GenerateLocalHandler(IReadOnlyCollection<ArgumentSyntax> methodParametersArgumentList, IMethodSymbol invokeMethod, string handlerName, ITypeSymbol returnType) =>

// Produces local function: void Handler(DataType1 eventParam1, DataType2 eventParam2) => eventHandler(eventParam1, eventParam2)
LocalFunctionStatement(
"void",
returnType.GenerateFullGenericName(),
"Handler",
invokeMethod.GenerateMethodParameters(),
ArrowExpressionClause(
InvocationExpression(handlerName, methodParametersArgumentList)));
returnType.SpecialType == SpecialType.System_Void ? GenerateArrowExpressionClauseForSyncronousHandler(handlerName, methodParametersArgumentList) : default,
returnType.SpecialType != SpecialType.System_Void ? GetBlockSyntaxForAsynconousHandler(handlerName, methodParametersArgumentList, returnType) : default);

private static ArrowExpressionClauseSyntax GenerateArrowExpressionClauseForSyncronousHandler(string handlerName, IReadOnlyCollection<ArgumentSyntax> methodParametersArgumentList) =>
ArrowExpressionClause(InvocationExpression(handlerName, methodParametersArgumentList));

private static BlockSyntax GetBlockSyntaxForAsynconousHandler(string handlerName, IReadOnlyCollection<ArgumentSyntax> methodParametersArgumentList, ITypeSymbol returnType) =>
Block(
new StatementSyntax[]
{
ExpressionStatement(
InvocationExpression(handlerName, methodParametersArgumentList)),
ReturnStatement(GetExpressionSyntax(returnType))
},
3);

private static ExpressionSyntax GetExpressionSyntax(ITypeSymbol returnType)
{
if (returnType.Name == nameof(Task))
{
return MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, returnType.GenerateFullGenericName(), IdentifierName("CompletedTask"));
}
else if (returnType.Name == nameof(ValueTask))
{
return SyntaxFactory.DefaultExpression(IdentifierName(returnType.GenerateFullGenericName()));
}

throw new NotSupportedException($"The return type of {returnType.GenerateFullGenericName()} is not supported!");
}

private static AssignmentExpressionSyntax GenerateEventAssignment(SyntaxKind accessor, string eventName, string dataObjectName, string handlerName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
Expand All @@ -18,7 +18,8 @@ internal interface IEventSymbolGenerator
/// Generates a compilation unit based on generating event observable wrappers.
/// </summary>
/// <param name="item">The symbol to generate for.</param>
/// <param name="getSymbolOf">Gets the symbol of a given type name.</param>
/// <returns>The new compilation unit.</returns>
NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item);
NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item, Func<string, ITypeSymbol?> getSymbolOf);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

using Microsoft.CodeAnalysis;
Expand All @@ -20,15 +19,15 @@ internal class InstanceEventGenerator : EventGeneratorBase
private const string DataFieldName = "_data";

/// <inheritdoc />
public override NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item)
public override NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item, Func<string, ITypeSymbol?> getSymbolOf)
{
var namespaceName = item.ContainingNamespace.ToDisplayString(RoslynHelpers.SymbolDisplayFormat);

var eventWrapperes = GenerateEventWrapperClasses(item, item.GetEvents().ToArray()).ToList();
var eventWrapperes = GenerateEventWrapperClasses(item, item.GetEvents(getSymbolOf).ToArray()).ToList();

if (eventWrapperes.Count > 0)
{
return NamespaceDeclaration(namespaceName, eventWrapperes, true);
return NamespaceDeclaration(namespaceName, eventWrapperes, true);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ namespace ReactiveMarbles.ObservableEvents.SourceGenerator.EventGenerators.Gener
internal class StaticEventGenerator : EventGeneratorBase
{
/// <inheritdoc />
public override NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item)
public override NamespaceDeclarationSyntax? Generate(INamedTypeSymbol item, Func<string, ITypeSymbol?> getSymbolOf)
{
var eventWrapperMembers = new List<PropertyDeclarationSyntax>();

var namespaceName = item.ContainingNamespace.ToDisplayString(RoslynHelpers.SymbolDisplayFormat);

foreach (var eventDetail in item.GetEvents(true))
foreach (var eventDetail in item.GetEvents(getSymbolOf, true))
{
var eventWrapper = GenerateEventWrapperObservable(eventDetail, item.GenerateFullGenericName(), item.Name);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Text;

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

using static ReactiveMarbles.ObservableEvents.SourceGenerator.SyntaxFactoryHelpers;
Expand Down Expand Up @@ -86,7 +84,7 @@ public static IEnumerable<T> GetMembers<T>(this INamedTypeSymbol symbol)
}
}

public static IEnumerable<IEventSymbol> GetEvents(this INamedTypeSymbol item, bool staticEvents = false, bool includeInherited = true)
public static IEnumerable<IEventSymbol> GetEvents(this INamedTypeSymbol item, Func<string, ITypeSymbol?> getSymbolOf, bool staticEvents = false, bool includeInherited = true)
{
var baseClassWithEvents = includeInherited ? item.GetBasesWithCondition(RoslynHelpers.HasEvents) : Array.Empty<INamedTypeSymbol>();

Expand Down Expand Up @@ -132,17 +130,17 @@ public static IEnumerable<IEventSymbol> GetEvents(this INamedTypeSymbol item, bo

var invokeMethod = ((INamedTypeSymbol)eventSymbol.OriginalDefinition.Type).DelegateInvokeMethod;

if (invokeMethod == null)
if (invokeMethod == null || ((invokeMethod.ReturnType as INamedTypeSymbol)?.IsGenericType ?? false))
{
continue;
}

if (invokeMethod.ReturnType.SpecialType != SpecialType.System_Void)
if (invokeMethod.ReturnType.SpecialType == SpecialType.System_Void ||
SymbolEqualityComparer.Default.Equals(invokeMethod.ReturnType, getSymbolOf("System.Threading.Tasks.Task")) ||
SymbolEqualityComparer.Default.Equals(invokeMethod.ReturnType, getSymbolOf("System.Threading.Tasks.ValueTask")))
{
continue;
yield return eventSymbol;
}

yield return eventSymbol;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,11 +598,11 @@ public static SyntaxList<TNode> List<TNode>(IReadOnlyCollection<TNode>? nodes, i
public static LocalDeclarationStatementSyntax LocalDeclarationStatement(VariableDeclarationSyntax declaration) => SyntaxFactory.LocalDeclarationStatement(default, default, default, default, declaration, Token(SyntaxKind.SemicolonToken));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static LocalFunctionStatementSyntax LocalFunctionStatement(string returnType, string functionName, IReadOnlyCollection<ParameterSyntax>? parameters, ArrowExpressionClauseSyntax expressionBody)
public static LocalFunctionStatementSyntax LocalFunctionStatement(string returnType, string functionName, IReadOnlyCollection<ParameterSyntax>? parameters, ArrowExpressionClauseSyntax? expressionBody = default, BlockSyntax? blockSyntax = default)
{
var parameterList = ParameterList(parameters);

return SyntaxFactory.LocalFunctionStatement(default, default, IdentifierName(returnType).AddTrialingSpaces(), Identifier(functionName), default, parameterList, default, default, expressionBody, Token(SyntaxKind.SemicolonToken));
return SyntaxFactory.LocalFunctionStatement(default, default, IdentifierName(returnType).AddTrialingSpaces(), Identifier(functionName), default, parameterList, default, blockSyntax, expressionBody, Token(SyntaxKind.SemicolonToken));
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down

0 comments on commit 5780647

Please sign in to comment.