소스 검색

SubtypesFactory generator

Steven He 3 년 전
부모
커밋
0bb22cff12

+ 28 - 1
Avalonia.sln

@@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE
 	ProjectSection(SolutionItems) = preProject
 		.editorconfig = .editorconfig
 		src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs
+		src\Shared\RawEventGrouping.cs = src\Shared\RawEventGrouping.cs
+		src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs
 	EndProjectSection
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}"
@@ -209,10 +211,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}"
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}"
 EndProject
 Global
@@ -1929,6 +1932,30 @@ Global
 		{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU
 		{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.Build.0 = Release|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
 		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
 		{2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU

+ 6 - 0
build/SourceGenerators.props

@@ -0,0 +1,6 @@
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <ProjectReference Include="$(MSBuildThisFileDirectory)/../src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
+    <Compile Include="$(MSBuildThisFileDirectory)/../src/Shared/SourceGeneratorAttributes.cs" />
+  </ItemGroup>
+</Project>

+ 13 - 22
src/Avalonia.Base/Animation/Easings/Easing.cs

@@ -1,8 +1,9 @@
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
-using System.Linq;
+using Avalonia.SourceGenerator;
 
 namespace Avalonia.Animation.Easings
 {
@@ -10,14 +11,15 @@ namespace Avalonia.Animation.Easings
     /// Base class for all Easing classes.
     /// </summary>
     [TypeConverter(typeof(EasingTypeConverter))]
-    public abstract class Easing : IEasing
+    public abstract partial class Easing : IEasing
     {
         /// <inheritdoc/>
         public abstract double Ease(double progress);
 
-        static Dictionary<string, Type>? _easingTypes;
+        private const string Namespace = "Avalonia.Animation.Easings";
 
-        static readonly Type s_thisType = typeof(Easing);
+        [SubtypesFactory(typeof(Easing), Namespace)]
+        private static partial bool TryCreateEasingInstance(string type, [NotNullWhen(true)] out Easing? instance);
 
         /// <summary>
         /// Parses a Easing type string.
@@ -26,33 +28,22 @@ namespace Avalonia.Animation.Easings
         /// <returns>Returns the instance of the parsed type.</returns>
         public static Easing Parse(string e)
         {
+#if NETSTANDARD2_0
+            if (e.Contains(","))
+#else
             if (e.Contains(','))
+#endif
             {
                 return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture));
             }
 
-            if (_easingTypes == null)
+            if (TryCreateEasingInstance(e, out var easing))
             {
-                _easingTypes = new Dictionary<string, Type>();
-
-                // Fetch the built-in easings.
-                var derivedTypes = typeof(Easing).Assembly.GetTypes()
-                                      .Where(p => p.Namespace == s_thisType.Namespace)
-                                      .Where(p => p.IsSubclassOf(s_thisType))
-                                      .Select(p => p);
-
-                foreach (var easingType in derivedTypes)
-                    _easingTypes.Add(easingType.Name, easingType);
-            }
-
-            if (_easingTypes.ContainsKey(e))
-            {
-                var type = _easingTypes[e];
-                return (Easing)Activator.CreateInstance(type)!;
+                return easing;
             }
             else
             {
-                throw new FormatException($"Easing \"{e}\" was not found in {s_thisType.Namespace} namespace.");
+                throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace.");
             }
         }
     }

+ 1 - 0
src/Avalonia.Base/Avalonia.Base.csproj

@@ -16,4 +16,5 @@
   <Import Project="..\..\build\ApiDiff.props" />
   <Import Project="..\..\build\NullableEnable.props" />
   <Import Project="..\..\build\DevAnalyzers.props" />
+  <Import Project="..\..\build\SourceGenerators.props" />
 </Project>

+ 8 - 0
src/Avalonia.Base/Input/KeyGesture.cs

@@ -155,7 +155,11 @@ namespace Avalonia.Input
             if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv))
                 return rv;
 
+#if NETSTANDARD2_0
             return (Key)Enum.Parse(typeof(Key), key, true);
+#else
+            return Enum.Parse<Key>(key, true);
+#endif
         }
 
         private static KeyModifiers ParseModifier(ReadOnlySpan<char> modifier)
@@ -172,7 +176,11 @@ namespace Avalonia.Input
                 return KeyModifiers.Meta;
             }
 
+#if NETSTANDARD2_0
             return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true);
+#else
+            return Enum.Parse<KeyModifiers>(modifier.ToString(), true);
+#endif
         }
 
         private Key ResolveNumPadOperationKey(Key key)

+ 8 - 0
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@@ -320,15 +320,23 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
             ? null
             : modifiersText
                 .Split(',')
+#if NETSTANDARD2_0
                 .Select(x => (InputProtocol.InputModifiers)Enum.Parse(
                     typeof(InputProtocol.InputModifiers), x, true))
+#else
+                .Select(x => Enum.Parse<InputProtocol.InputModifiers>(x, true))
+#endif
                 .ToArray();
 
         private static InputProtocol.MouseButton ParseMouseButton(string buttonText) =>
             string.IsNullOrWhiteSpace(buttonText)
             ? InputProtocol.MouseButton.None
+#if NETSTANDARD2_0
             : (InputProtocol.MouseButton)Enum.Parse(
                 typeof(InputProtocol.MouseButton), buttonText, true);
+#else
+            : Enum.Parse<InputProtocol.MouseButton>(buttonText, true);
+#endif
 
         private static double ParseDouble(string text) =>
             double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);

+ 17 - 0
src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj

@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
+    <Compile Include="..\Shared\SourceGeneratorAttributes.cs" />
+  </ItemGroup>
+
+</Project>

+ 167 - 0
src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs

@@ -0,0 +1,167 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Avalonia.SourceGenerator
+{
+    internal class GenerateSubtypesSyntaxReceiver : ISyntaxReceiver
+    {
+        public List<(MethodDeclarationSyntax, AttributeSyntax)> CandidateMethods { get; } = new();
+        public List<SyntaxNode> Types { get; } = new();
+
+        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
+        {
+            if (syntaxNode is MethodDeclarationSyntax declarationSyntax)
+            {
+                foreach (var attribute in declarationSyntax.AttributeLists.SelectMany(i => i.Attributes))
+                {
+                    CandidateMethods.Add((declarationSyntax, attribute));
+                }
+            }
+
+            if (syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax)
+            {
+                Types.Add(syntaxNode);
+            }
+        }
+    }
+
+    [Generator]
+    internal class SubtypesFactoryGenerator : ISourceGenerator
+    {
+        private readonly GenerateSubtypesSyntaxReceiver _receiver = new();
+        private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName;
+
+        public void Execute(GeneratorExecutionContext context)
+        {
+            var methods = new List<(IMethodSymbol, ITypeSymbol, string)>();
+
+            foreach (var (method, attribute) in _receiver.CandidateMethods)
+            {
+                var semanticModel = context.Compilation.GetSemanticModel(method.SyntaxTree);
+                var attributeTypeInfo = semanticModel.GetTypeInfo(attribute);
+                if (attributeTypeInfo.Type is null ||
+                    attributeTypeInfo.Type.ToString() != s_attributeName ||
+                    attribute.ArgumentList is null)
+                {
+                    continue;
+                }
+
+                var arguments = attribute.ArgumentList.Arguments;
+                if (arguments.Count != 2)
+                {
+                    continue;
+                }
+
+                if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr ||
+                    arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax)
+                {
+                    continue;
+                }
+
+                var type = semanticModel.GetTypeInfo(typeOfExpr.Type);
+                var ns = semanticModel.GetConstantValue(arguments[1].Expression);
+                var methodDeclInfo = semanticModel.GetDeclaredSymbol(method);
+
+                if (type.Type is not ITypeSymbol baseType ||
+                    ns.HasValue is false ||
+                    ns.Value is not string nsValue ||
+                    methodDeclInfo is not IMethodSymbol methodSymbol ||
+                    methodSymbol.Parameters.Length != 2 ||
+                    methodSymbol.Parameters[1].RefKind != RefKind.Out)
+                {
+                    continue;
+                }
+
+                methods.Add((methodSymbol, baseType, nsValue));
+            }
+
+            var types = new List<ITypeSymbol>();
+            foreach (var type in _receiver.Types)
+            {
+                var semanticModel = context.Compilation.GetSemanticModel(type.SyntaxTree);
+                var decl = semanticModel.GetDeclaredSymbol(type);
+                if (decl is ITypeSymbol typeSymbol)
+                {
+                    types.Add(typeSymbol);
+                }
+            }
+
+            GenerateSubTypes(context, methods, types);
+        }
+
+        private bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType)
+        {
+            if (type.BaseType is null)
+            {
+                return false;
+            }
+
+            if (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType))
+            {
+                return true;
+            }
+
+            return IsSubtypeOf(type.BaseType, baseType);
+        }
+
+        private void GenerateSubTypes(
+            GeneratorExecutionContext context,
+            List<(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace)> methods,
+            List<ITypeSymbol> types)
+        {
+            foreach (var (method, baseType, @namespace) in methods)
+            {
+                var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray();
+                var type = method.ContainingType;
+                var isGeneric = type.TypeParameters.Length > 0;
+                var isClass = type.TypeKind == TypeKind.Class;
+
+                if (method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax methodDecl)
+                {
+                    continue;
+                }
+
+                var parameters = new SeparatedSyntaxList<ParameterSyntax>().AddRange(methodDecl.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList<AttributeListSyntax>())));
+
+                var methodDeclText = methodDecl
+                    .WithAttributeLists(new SyntaxList<AttributeListSyntax>())
+                    .WithParameterList(methodDecl.ParameterList.WithParameters(parameters))
+                    .WithBody(null)
+                    .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None))
+                    .WithoutTrivia().ToString();
+
+                var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}";
+                var source = $@"using System;
+using System.Collections.Generic;
+
+namespace {method.ContainingNamespace}
+{{
+    {typeDecl}
+    {{
+        {methodDeclText}
+        {{
+            var hasMatch = false;
+            (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch
+            {{
+{string.Join("\n", candidateTypes.Select(i => $"                \"{i.Name}\" => (true, ({method.Parameters[1].Type})new {i}()),"))}
+                _ => (false, default({method.Parameters[1].Type}))
+            }};
+
+            return hasMatch;
+        }}
+    }}
+}}";
+
+                context.AddSource($"{type}.{method.MetadataName}.gen.cs", source);
+            }
+        }
+
+        public void Initialize(GeneratorInitializationContext context)
+        {
+            context.RegisterForSyntaxNotifications(() => _receiver);
+        }
+    }
+}

+ 17 - 0
src/Shared/SourceGeneratorAttributes.cs

@@ -0,0 +1,17 @@
+using System;
+
+namespace Avalonia.SourceGenerator
+{
+    [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
+    internal sealed class SubtypesFactoryAttribute : Attribute
+    {
+        public SubtypesFactoryAttribute(Type baseType, string @namespace)
+        {
+            BaseType = baseType;
+            Namespace = @namespace;
+        }
+
+        public string Namespace { get; }
+        public Type BaseType { get; }
+    }
+}