Browse Source

SubtypesFactory generator

pull/7935/head
Steven He 4 years ago
parent
commit
0bb22cff12
  1. 29
      Avalonia.sln
  2. 6
      build/SourceGenerators.props
  3. 35
      src/Avalonia.Base/Animation/Easings/Easing.cs
  4. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  5. 8
      src/Avalonia.Base/Input/KeyGesture.cs
  6. 8
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  7. 17
      src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj
  8. 167
      src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs
  9. 17
      src/Shared/SourceGeneratorAttributes.cs

29
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
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>

35
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
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
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
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
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
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
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; }
}
}
Loading…
Cancel
Save