-
Notifications
You must be signed in to change notification settings - Fork 760
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
perf: ImplementedRoutedEvents evaluated at compile-time via a source …
…generator # Conflicts: # src/Uno.UI/UI/Xaml/Controls/Control/Control.cs
- Loading branch information
1 parent
d690944
commit f2b18d9
Showing
3 changed files
with
260 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
...ators/Uno.UI.SourceGenerators/ImplementedRoutedEvents/ImplementedRoutedEventsGenerator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
#nullable enable | ||
|
||
using System; | ||
using System.Linq; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Uno.UI.SourceGenerators.Helpers; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
|
||
#if NETFRAMEWORK | ||
using Uno.SourceGeneration; | ||
#endif | ||
|
||
|
||
namespace Uno.UI.SourceGenerators.ImplementedRoutedEvents | ||
{ | ||
[Generator] | ||
public class ImplementedRoutedEventsGenerator : ISourceGenerator | ||
{ | ||
private const string GetImplementedRoutedEventsMethodName = "GetImplementedRoutedEvents"; | ||
|
||
public void Initialize(GeneratorInitializationContext context) | ||
{ | ||
// No initialization required. | ||
} | ||
|
||
public void Execute(GeneratorExecutionContext context) | ||
{ | ||
if (!DesignTimeHelper.IsDesignTime(context)) | ||
{ | ||
var controlSymbol = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.Controls.Control"); | ||
if (controlSymbol is null) | ||
{ | ||
return; | ||
} | ||
|
||
var getImplementedRoutedEventsSymbol = controlSymbol.GetMembers(GetImplementedRoutedEventsMethodName).SingleOrDefault(m => m.Kind == SymbolKind.Method) as IMethodSymbol; | ||
if (getImplementedRoutedEventsSymbol is null) | ||
{ | ||
return; | ||
} | ||
|
||
if (!getImplementedRoutedEventsSymbol.ContainingAssembly.IsSameAssemblyOrHasFriendAccessTo(context.Compilation.Assembly)) | ||
{ | ||
return; | ||
} | ||
|
||
var visitor = new ControlTypesVisitor(context, getImplementedRoutedEventsSymbol); | ||
visitor.Visit(context.Compilation.SourceModule); | ||
} | ||
} | ||
|
||
private class ControlTypesVisitor : SymbolVisitor | ||
{ | ||
private readonly GeneratorExecutionContext _context; | ||
private readonly IMethodSymbol _getImplementedRoutedEvents; | ||
private readonly HashSet<INamedTypeSymbol> _seenTypes = new HashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default); | ||
|
||
private readonly Dictionary<string, INamedTypeSymbol> _events = new Dictionary<string, INamedTypeSymbol>(); | ||
|
||
public ControlTypesVisitor(GeneratorExecutionContext context, IMethodSymbol getImplementedRoutedEventsSymbol) | ||
{ | ||
_context = context; | ||
_getImplementedRoutedEvents = getImplementedRoutedEventsSymbol; | ||
var pointerRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.PointerRoutedEventArgs", context.Compilation); | ||
_events.Add("OnPointerPressed", pointerRoutedEventArgs); | ||
_events.Add("OnPointerReleased", pointerRoutedEventArgs); | ||
_events.Add("OnPointerEntered", pointerRoutedEventArgs); | ||
_events.Add("OnPointerExited", pointerRoutedEventArgs); | ||
_events.Add("OnPointerMoved", pointerRoutedEventArgs); | ||
_events.Add("OnPointerCanceled", pointerRoutedEventArgs); | ||
_events.Add("OnPointerCaptureLost", pointerRoutedEventArgs); | ||
_events.Add("OnPointerWheelChanged", pointerRoutedEventArgs); | ||
|
||
var manipulationStartingRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.ManipulationStartingRoutedEventArgs", context.Compilation); | ||
_events.Add("OnManipulationStarting", manipulationStartingRoutedEventArgs); | ||
|
||
var manipulationStartedRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.ManipulationStartedRoutedEventArgs", context.Compilation); | ||
_events.Add("OnManipulationStarted", manipulationStartedRoutedEventArgs); | ||
|
||
var manipulationDeltaRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.ManipulationDeltaRoutedEventArgs", context.Compilation); | ||
_events.Add("OnManipulationDelta", manipulationDeltaRoutedEventArgs); | ||
|
||
var manipulationInertiaStartingRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.ManipulationInertiaStartingRoutedEventArgs", context.Compilation); | ||
_events.Add("OnManipulationInertiaStarting", manipulationInertiaStartingRoutedEventArgs); | ||
|
||
var manipulationCompletedRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.ManipulationCompletedRoutedEventArgs", context.Compilation); | ||
_events.Add("OnManipulationCompleted", manipulationCompletedRoutedEventArgs); | ||
|
||
var tappedRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.TappedRoutedEventArgs", context.Compilation); | ||
_events.Add("OnTapped", tappedRoutedEventArgs); | ||
|
||
var doubleTappedRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.DoubleTappedRoutedEventArgs", context.Compilation); | ||
_events.Add("OnDoubleTapped", doubleTappedRoutedEventArgs); | ||
|
||
var rightTappedRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.RightTappedRoutedEventArgs", context.Compilation); | ||
_events.Add("OnRightTapped", rightTappedRoutedEventArgs); | ||
|
||
var holdingRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.HoldingRoutedEventArgs", context.Compilation); | ||
_events.Add("OnHolding", holdingRoutedEventArgs); | ||
|
||
var dragEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.DragEventArgs", context.Compilation); | ||
_events.Add("OnDragEnter", dragEventArgs); | ||
_events.Add("OnDragOver", dragEventArgs); | ||
_events.Add("OnDragLeave", dragEventArgs); | ||
_events.Add("OnDrop", dragEventArgs); | ||
|
||
var keyRoutedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.Input.KeyRoutedEventArgs", context.Compilation); | ||
_events.Add("OnKeyDown", keyRoutedEventArgs); | ||
_events.Add("OnKeyUp", keyRoutedEventArgs); | ||
|
||
var routedEventArgs = GetRequiredTypeByMetadataName("Windows.UI.Xaml.RoutedEventArgs", context.Compilation); | ||
_events.Add("OnLostFocus", routedEventArgs); | ||
_events.Add("OnGotFocus", routedEventArgs); | ||
} | ||
|
||
private static INamedTypeSymbol GetRequiredTypeByMetadataName(string fullyQualifiedMetadataName, Compilation compilation) | ||
{ | ||
var type = compilation.GetTypeByMetadataName(fullyQualifiedMetadataName); | ||
return type ?? throw new InvalidOperationException($"The type '{fullyQualifiedMetadataName}' is not found."); | ||
} | ||
|
||
public override void VisitModule(IModuleSymbol symbol) => VisitNamespace(symbol.GlobalNamespace); | ||
|
||
public override void VisitNamedType(INamedTypeSymbol symbol) | ||
{ | ||
foreach (var type in symbol.GetTypeMembers()) | ||
{ | ||
VisitNamedType(type); | ||
} | ||
|
||
ProcessType(symbol); | ||
} | ||
|
||
public override void VisitNamespace(INamespaceSymbol symbol) | ||
{ | ||
foreach (var n in symbol.GetNamespaceMembers()) | ||
{ | ||
VisitNamespace(n); | ||
} | ||
|
||
foreach (var t in symbol.GetTypeMembers()) | ||
{ | ||
VisitNamedType(t); | ||
} | ||
} | ||
|
||
private void ProcessType(INamedTypeSymbol type) | ||
{ | ||
// Control is defining the virtual property, we cannot generate an override for it. | ||
// Use of _seenTypes to prevent processing the same type twice, which causes warnings like: | ||
// warning CS2002: Source file 'src\Uno.UI\obj\Debug\monoandroid11.0\g\ImplementedRoutedEventsGenerator\TwoPaneView_ImplementedRoutedEvents_g_cs.g.cs' specified multiple times | ||
if (!type.IsAbstract && type.Is(_getImplementedRoutedEvents.ContainingType) && | ||
!type.Equals(_getImplementedRoutedEvents.ContainingType, SymbolEqualityComparer.Default) && | ||
_seenTypes.Add(type)) | ||
{ | ||
var list = new List<string>(); | ||
list.Add("global::Uno.UI.Xaml.RoutedEventFlag.None"); | ||
|
||
foreach (var @event in _events) | ||
{ | ||
if (!@event.Key.StartsWith("On", StringComparison.Ordinal)) | ||
{ | ||
throw new InvalidOperationException("Expected event to start with 'On'"); | ||
} | ||
|
||
if (IsEventOverrideImplemented(type, @event.Key, @event.Value)) | ||
{ | ||
list.Add($"global::Uno.UI.Xaml.RoutedEventFlag.{@event.Key.Substring(2)}"); | ||
} | ||
} | ||
|
||
GenerateCode(type, list); | ||
} | ||
} | ||
|
||
private bool IsEventOverrideImplemented(INamedTypeSymbol type, string name, INamedTypeSymbol arg) | ||
{ | ||
if (type.Equals(_getImplementedRoutedEvents.ContainingType, SymbolEqualityComparer.Default)) | ||
{ | ||
return false; | ||
} | ||
|
||
var method = type.GetMembers(name).SingleOrDefault( | ||
m => m is IMethodSymbol method && method.Parameters.Length == 1 && arg.Equals(method.Parameters[0].Type, SymbolEqualityComparer.Default)) as IMethodSymbol; | ||
// base type can't be null, so the suppression should be safe. We know that the initial type inherits Control class (ie, _implementedRoutedEvents.ContainingType) | ||
// And the recursion will end as soon as we get there. | ||
return method?.IsOverride == true || IsEventOverrideImplemented(type.BaseType!, name, arg); | ||
} | ||
|
||
private void GenerateCode(INamedTypeSymbol type, IEnumerable<string> routedEventFlags) | ||
{ | ||
// Keep containing namespace here to avoid controls defined in both WUXC and MUXC from one overwriting the other. | ||
_context.AddSource($"{type.ContainingNamespace}.{type.Name}_ImplementedRoutedEvents.g.cs", $@"// <auto-generated> | ||
namespace {type.ContainingNamespace} | ||
{{ | ||
partial class {type.Name} | ||
{{ | ||
protected override global::Uno.UI.Xaml.RoutedEventFlag {GetImplementedRoutedEventsMethodName}() | ||
{{ | ||
return {string.Join(" | ", routedEventFlags)}; | ||
}} | ||
}} | ||
}} | ||
"); | ||
} | ||
} | ||
} | ||
} |
41 changes: 41 additions & 0 deletions
41
...Tests/ImplementedRoutedEventArgsGeneratorTest/ImplementedRoutedEventArgsGeneratorTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#if !WINDOWS_UWP | ||
|
||
using System.Reflection; | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
using Uno.UI.Xaml; | ||
using Windows.UI.Xaml.Controls; | ||
|
||
namespace Uno.UI.RuntimeTests.Tests.ImplementedRoutedEventArgsGeneratorTest | ||
{ | ||
[TestClass] | ||
public partial class ImplementedRoutedEventArgsGeneratorTests | ||
{ | ||
private MethodInfo GetMethod(Control control) | ||
{ | ||
return control.GetType().GetMethod("GetImplementedRoutedEvents", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); | ||
} | ||
|
||
[TestMethod] | ||
public void Given_Control() | ||
{ | ||
var control = new Control(); | ||
var method = GetMethod(control); | ||
Assert.AreEqual(RoutedEventFlag.None, (RoutedEventFlag)method.Invoke(control, null)); | ||
} | ||
|
||
[TestMethod] | ||
public void Given_Button() | ||
{ | ||
var btn = new Button(); | ||
var method = GetMethod(btn); | ||
|
||
// Make sure the generator is actually used and is generating source. We don't want the test to be running the virtual method. | ||
Assert.AreEqual(typeof(Button), method.DeclaringType); | ||
|
||
// It's okay to adjust the following assert if more events are needed and implemented in the future. | ||
// This test is focused more of the source generator rather than Button functionality. | ||
Assert.AreEqual(RoutedEventFlag.PointerEntered | RoutedEventFlag.PointerPressed | RoutedEventFlag.PointerReleased, (RoutedEventFlag)method.Invoke(btn, null)); | ||
} | ||
} | ||
} | ||
#endif |