From 04005d9cb67bb7053b174cf7711b47ea55a3d1e6 Mon Sep 17 00:00:00 2001 From: OMGeeky Date: Thu, 2 Nov 2023 22:26:03 +0100 Subject: [PATCH] Initial commit Included generators: - an attribute that allows for easy GetComponent with just one method for all UI: - Attribute for generating UxmlTraits and needed stuff for custom UIElement-attributes/fields - Attribute for easily getting Elements from the Uxml file with a single method for all --- .../Unity/Components/GetComponentGenerator.cs | 142 +++++++ .../Unity/Ui/UIBackingClassGenerator.cs | 363 ++++++++++++++++++ TestConsole/Program.cs | 31 ++ TestConsole/TestConsole.csproj | 14 + TestGenerators.sln | 22 ++ 5 files changed, 572 insertions(+) create mode 100644 ExampleGenerator/Unity/Components/GetComponentGenerator.cs create mode 100644 ExampleGenerator/Unity/Ui/UIBackingClassGenerator.cs create mode 100644 TestConsole/Program.cs create mode 100644 TestConsole/TestConsole.csproj create mode 100644 TestGenerators.sln diff --git a/ExampleGenerator/Unity/Components/GetComponentGenerator.cs b/ExampleGenerator/Unity/Components/GetComponentGenerator.cs new file mode 100644 index 0000000..2abaa91 --- /dev/null +++ b/ExampleGenerator/Unity/Components/GetComponentGenerator.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + + +namespace ExampleGenerator.Unity.Components +{ + [Generator] + public class GetComponentGenerator : ISourceGenerator + { + private const string _attributeText = @" +using System; + +[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)] +internal class GetComponentAttribute : Attribute +{ + public enum TargetType + { + This = 0, + Parent = 1, + Child = 2, + } + + public GetComponentAttribute(TargetType targetType = TargetType.This) + { + } +} +"; + + public void Initialize( GeneratorInitializationContext context ) + { + context.RegisterForPostInitialization( i => i.AddSource( "GetComponentAttribute_g.cs" , _attributeText ) ); + context.RegisterForSyntaxNotifications( () => new SyntaxReceiver() ); + } + + public void Execute( GeneratorExecutionContext context ) + { + if ( !(context.SyntaxContextReceiver is SyntaxReceiver receiver) ) + return; + + INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName( "GetComponentAttribute" ); + + foreach ( IGrouping group in receiver.Fields + .GroupBy( f => f.ContainingType + , SymbolEqualityComparer.Default ) ) + { + var classSource = ProcessClass( group.Key , group , attributeSymbol ); + context.AddSource( $"{group.Key.Name}_Components_g.cs" , SourceText.From( classSource , Encoding.UTF8 ) ); + } + } + + private string ProcessClass( INamedTypeSymbol classSymbol , IEnumerable fields , ISymbol attributeSymbol ) + { + var source = new StringBuilder( $@" + +public partial class {classSymbol.Name} +{{ +private void t() +{{ +" ); + + foreach ( IFieldSymbol fieldSymbol in fields ) + { + ProcessField( source , fieldSymbol , attributeSymbol ); + } + + source.Append( "}\n\n}" ); + return source.ToString(); + } + + private void ProcessField( StringBuilder source , IFieldSymbol fieldSymbol , ISymbol attributeSymbol ) + { + var fieldName = fieldSymbol.Name; + ITypeSymbol fieldType = fieldSymbol.Type; + + AttributeData attributeData = fieldSymbol.GetAttributes() + .Single( ad => + ad.AttributeClass.Equals( attributeSymbol , SymbolEqualityComparer.Default ) ); + + var methodType = ProcessAttribute( attributeData ); + + source.AppendLine( $@"{fieldName} = {methodType}<{fieldType}>();" ); + } + + private string ProcessAttribute( AttributeData attributeData ) + { + var stringBuilder = new StringBuilder( "GetComponent" ); + if ( attributeData.ConstructorArguments.Length > 0 + && int.TryParse( attributeData.ConstructorArguments[0].Value.ToString() , out var enumValue ) ) + { + if ( enumValue == 1 ) + stringBuilder.Append( "InParent" ); + + if ( enumValue == 2 ) + stringBuilder.Append( "InChildren" ); + } + + return stringBuilder.ToString(); + } + } + + internal class SyntaxReceiver : ISyntaxContextReceiver + { + public List Fields { get; } = new List(); + + public void OnVisitSyntaxNode( GeneratorSyntaxContext context ) + { + if ( context.Node is FieldDeclarationSyntax fieldDeclarationSyntax && fieldDeclarationSyntax.AttributeLists.Count > 0 ) + { + foreach ( VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables ) + { + IFieldSymbol fieldSymbol = context.SemanticModel.GetDeclaredSymbol( variable ) as IFieldSymbol; + + if ( IsDerivedFrom( fieldSymbol?.ContainingType.BaseType , "MonoBehaviour" ) + && IsDerivedFrom( fieldSymbol?.Type.BaseType , "Component" ) + && fieldSymbol.GetAttributes() + .Any( ad => ad.AttributeClass.ToDisplayString() == "GetComponentAttribute" ) ) + { + Fields.Add( fieldSymbol ); + } + } + } + } + + private bool IsDerivedFrom( INamedTypeSymbol baseType , string targetType ) + { + while ( baseType != null ) + { + if ( baseType.Name == targetType ) + return true; + + baseType = baseType.BaseType; + } + + return false; + } + } +} diff --git a/ExampleGenerator/Unity/Ui/UIBackingClassGenerator.cs b/ExampleGenerator/Unity/Ui/UIBackingClassGenerator.cs new file mode 100644 index 0000000..0401a4f --- /dev/null +++ b/ExampleGenerator/Unity/Ui/UIBackingClassGenerator.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +using System.Text; + +using Microsoft.CodeAnalysis.CSharp.Syntax; + + +namespace ExampleGenerator.Unity.Ui +{ + public static class Helpers + { + public const string UxmlTraitAttribute = "UxmlTraitAttribute"; + public const string UiElementAttribute = "UiElementAttribute"; + + internal static bool IsDerivedFrom( INamedTypeSymbol baseType , string targetType ) + { + while ( baseType != null ) + { + if ( baseType.Name == targetType ) + return true; + + baseType = baseType.BaseType; + } + + return false; + } + } + + [Generator] + public class UiBackingClassGenerator : ISourceGenerator + { + private static readonly string UxmlTraitAttributeText = $@"// +using System; +/// Helper attribute for UXML generation that generates the +/// UxmlTrait definitions needed for the UIElements. +/// +/// Works on properties and fields +/// +/// When applied to a Property the uxml-fields only work and +/// save if the property has a backing field. If its an auto +/// property it won't save the changes in the UI-Builder and probably some other locations too. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = false)] +internal class {Helpers.UxmlTraitAttribute} : Attribute +{{ + public {Helpers.UxmlTraitAttribute}(string name, object defaultValue) {{ }} +}} +"; + + private static readonly string UiElementAttributeText = $@"// +using System; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true, AllowMultiple = false)] +internal class {Helpers.UiElementAttribute} : Attribute +{{ + public {Helpers.UiElementAttribute}(string name) {{ }} +}} +"; + + #region Implementation of ISourceGenerator + + public void Initialize( GeneratorInitializationContext context ) + { + context.RegisterForPostInitialization( i => + { + i.AddSource( $"{Helpers.UxmlTraitAttribute}_g.cs" + , SourceText.From( UxmlTraitAttributeText , Encoding.UTF8 ) ); + + i.AddSource( $"{Helpers.UiElementAttribute}_g.cs" + , SourceText.From( UiElementAttributeText , Encoding.UTF8 ) ); + } ); + + context.RegisterForSyntaxNotifications( () => new SyntaxReceiver() ); + } + + public void Execute( GeneratorExecutionContext context ) + { + if ( !(context.SyntaxContextReceiver is SyntaxReceiver receiver) ) + return; + + INamedTypeSymbol uxmlTraitAttributeSymbol = context.Compilation.GetTypeByMetadataName( Helpers.UxmlTraitAttribute ); + INamedTypeSymbol uiElementAttributeSymbol = context.Compilation.GetTypeByMetadataName( Helpers.UiElementAttribute ); + foreach ( IGrouping group in receiver.Fields + .GroupBy( f => f.ContainingType + , SymbolEqualityComparer.Default ) ) + { + var classSource = ProcessClass( group.Key , group , uxmlTraitAttributeSymbol , uiElementAttributeSymbol ); + if ( classSource == null ) + continue; + + context.AddSource( $"{group.Key.Name}_ui_g.cs" , SourceText.From( classSource , Encoding.UTF8 ) ); + } + } + + private string ProcessClass( INamedTypeSymbol classSymbol + , IEnumerable fields + , INamedTypeSymbol uxmlTraitAttributeSymbol + , INamedTypeSymbol uiElementAttributeSymbol ) + { + var fieldsList = fields.ToList(); + if ( !fieldsList.Any() ) + return null; + + List elementFields = fieldsList.Where( f => GetUiElementAttributeData( f , uiElementAttributeSymbol ) != null ).ToList(); + + foreach ( var VARIABLE in elementFields ) + { + // + } + + var uxmlTraitFields = fieldsList.Where( f => GetUxmlTraitAttributeData( f , uxmlTraitAttributeSymbol ) != null ).ToList(); + var source = new StringBuilder( $@"// + +using UnityEngine.UIElements; +namespace {classSymbol.ContainingNamespace} +{{ +public partial class {classSymbol.Name} +{{ + public new class UxmlFactory : UxmlFactory<{classSymbol.Name}, UxmlTraits> {{ }} + public new class UxmlTraits : VisualElement.UxmlTraits + {{ +" ); + + // throw new NotImplementedException( $"elements: {elementFields.Count} uxmlTraits: {uxmlTraitFields.Count}" ); + foreach ( ISymbol fieldSymbol in uxmlTraitFields ) + { + source.AppendLine( GetAttributeDescription( fieldSymbol , uxmlTraitAttributeSymbol ) ); + } + + source.Append( $@" + public override void Init(VisualElement ve , IUxmlAttributes bag , CreationContext cc ) + {{ + base.Init( ve , bag , cc ); + var self = ({classSymbol.Name}) ve; + +" ); + + foreach ( ISymbol fieldSymbol in uxmlTraitFields ) + { + source.AppendLine( GetAttributeInitialization( fieldSymbol , uxmlTraitAttributeSymbol ) ); + } + + source.Append( $@" }} + }} + public void QueryComponents() + {{ +" ); + + foreach ( ISymbol fieldSymbol in elementFields ) + { + // source.AppendLine( $" {fieldSymbol.Name} = this.Q<{GetQualifyingTypeNameFromSymbol( fieldSymbol )}>(\"hi\");" ); + + source.AppendLine( $" {fieldSymbol.Name} = this.Q<{GetQualifyingTypeNameFromSymbol( fieldSymbol )}>(\"{GetUiElementAttributeData( fieldSymbol , uiElementAttributeSymbol )?.Name}\");" ); + } + + source.Append( $@" }} +}} +}} +" ); + + + return source.ToString(); + } + + private string GetTypeNameFromSymbol( ISymbol symbol ) => GetTypeName( GetTypeFromSymbol( symbol ) ); + private string GetQualifyingTypeNameFromSymbol( ISymbol symbol ) => GetQualifyingTypeName( GetTypeFromSymbol( symbol ) ); + + private static UiElementAttributeData? GetUiElementAttributeData( ISymbol fieldSymbol , INamedTypeSymbol uiElementAttributeSymbol ) + { + var attr = GetSingleAttributeData( fieldSymbol , uiElementAttributeSymbol ); + if ( attr == null ) + return null; + + var args = attr.ConstructorArguments.ToList(); + if ( args.Count != 1 ) + { + throw new NotImplementedException( $"Attribute did not have enough parameters: expected 1 got {args.Count} {attr}: args: {args}" ); + } + + var name = args[0].Value as string; + return new UiElementAttributeData() + { + Name = name , + }; + } + + private static UxmlTraitAttributeData? GetUxmlTraitAttributeData( ISymbol fieldSymbol , INamedTypeSymbol uxmlTraitAttributeSymbol ) + { + AttributeData attr = GetSingleAttributeData( fieldSymbol , uxmlTraitAttributeSymbol ); + if ( attr == null ) + return null; + + var args = attr.ConstructorArguments.ToList(); + if ( args.Count != 2 ) + { + throw new NotImplementedException( $"Attribute did not have enough parameters: expected 2 got {args.Count} {attr}: args: {args}" ); + } + + var name = args[0].Value as string; + var defaultValue = args[1].Value; + if ( defaultValue != null ) + { + defaultValue = defaultValue.ToString(); + if ( (string) defaultValue == "False" ) + defaultValue = "false"; + + if ( (string) defaultValue == "True" ) + defaultValue = "true"; + } + + var type = GetTypeFromSymbol( fieldSymbol ); + + return new UxmlTraitAttributeData() + { + Name = name , Type = type , defaultValue = defaultValue + }; + } + + private static AttributeData GetSingleAttributeData( ISymbol fieldSymbol , INamedTypeSymbol attributeSymbol ) + { + var attr = fieldSymbol.GetAttributes() + .SingleOrDefault( ad => + ad?.AttributeClass?.Equals( attributeSymbol , SymbolEqualityComparer.Default ) ?? false ); + + return attr; + } + + + private static ITypeSymbol GetTypeFromSymbol( ISymbol symbol ) + { + switch ( symbol ) + { + case IFieldSymbol fieldSymbol: + return fieldSymbol.Type; + + case IPropertySymbol propertySymbol: + return propertySymbol.Type; + + default: + throw new InvalidCastException( $"symbol was not property or field: {symbol}" ); + } + } + + struct UxmlTraitAttributeData + { + public string Name; + public ITypeSymbol Type; + public object defaultValue; + } + + struct UiElementAttributeData + { + public string Name; + } + + private string GetAttributeDescription( ISymbol fieldSymbol , INamedTypeSymbol attributeSymbol ) + { + // private UxmlIntAttributeDescription m_PlayerHealth = new() { name = "player-health" , defaultValue = 0 }; + var name = GetAttributeDescriptionName( fieldSymbol.Name ); + var attr = GetUxmlTraitAttributeData( fieldSymbol , attributeSymbol ).Value; + var type = ConvertTypeToUxmlAttributeDescriptionType( attr.Type ); + + var attributeName = attr.Name; + var defaultValue = attr.defaultValue; + return $" private {type} {name} = new() {{ name = \"{attributeName}\" , defaultValue = {defaultValue} }};"; + } + + private string ConvertTypeToUxmlAttributeDescriptionType( ITypeSymbol type ) + { + String typeString; + string typeName = GetTypeName( type ); + switch ( typeName ) + { + case "int": + typeString = "Int"; + break; + + case "bool": + typeString = "Bool"; + break; + + case "Color": + typeString = "Color"; + break; + + case "string": + typeString = "String"; + break; + + default: + Debug.WriteLine( $"Could not get type-name for type: {type.Name}" ); + typeString = type.Name; + break; + } + + return $"Uxml{typeString}AttributeDescription"; + } + + private static string GetTypeName( ITypeSymbol type ) { return type.ToDisplayString( SymbolDisplayFormat.MinimallyQualifiedFormat ); } + + private static string GetQualifyingTypeName( ITypeSymbol type ) { return type.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat ); } + + private string GetAttributeInitialization( ISymbol symbol , ISymbol attributeSymbol ) + { + // self.PlayerHealth = m_PlayerHealth.GetValueFromBag( bag , cc ); + return $" self.{symbol.Name} = {GetAttributeDescriptionName( symbol.Name )}.GetValueFromBag( bag , cc );"; + } + + private string GetAttributeDescriptionName( string name ) => $"m_{name}"; + + #endregion + + } + + public class SyntaxReceiver : ISyntaxContextReceiver + { + + public List Fields { get; } = new List(); + + #region Implementation of ISyntaxContextReceiver + + public void OnVisitSyntaxNode( GeneratorSyntaxContext context ) + { + if ( context.Node is FieldDeclarationSyntax fieldDeclarationSyntax && fieldDeclarationSyntax.AttributeLists.Count > 0 ) + { + foreach ( VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables ) + { + ISymbol symbol = context.SemanticModel.GetDeclaredSymbol( variable ) as IFieldSymbol; + + if ( Helpers.IsDerivedFrom( symbol?.ContainingType.BaseType , "AtVisualElement" ) + && symbol.GetAttributes() + .Any( ad => ad.AttributeClass?.ToDisplayString() == Helpers.UxmlTraitAttribute + || ad.AttributeClass?.ToDisplayString() == Helpers.UiElementAttribute ) ) + { + Fields.Add( symbol ); + } + } + } + + if ( context.Node is PropertyDeclarationSyntax propertyDeclarationSyntax && propertyDeclarationSyntax.AttributeLists.Count > 0 ) + { + ISymbol symbol = context.SemanticModel.GetDeclaredSymbol( propertyDeclarationSyntax ) as IPropertySymbol; + + if ( Helpers.IsDerivedFrom( symbol?.ContainingType.BaseType , "AtVisualElement" ) + && symbol.GetAttributes() + .Any( ad => ad.AttributeClass?.ToDisplayString() == Helpers.UxmlTraitAttribute + || ad.AttributeClass?.ToDisplayString() == Helpers.UiElementAttribute ) ) + { + Fields.Add( symbol ); + } + } + } + + #endregion + + } +} diff --git a/TestConsole/Program.cs b/TestConsole/Program.cs new file mode 100644 index 0000000..32aa389 --- /dev/null +++ b/TestConsole/Program.cs @@ -0,0 +1,31 @@ +// See https://aka.ms/new-console-template for more information + +using System.Diagnostics; + + +namespace ConsoleApp; + +partial class Program +{ + static void Main( string[] args ) { HelloFrom( "Generated Code" ); } + + static partial void HelloFrom( string name ); +} + +public partial class Test1 : AtVisualElement +{ + // [UxmlTrait( "health" , 9 )] + // public int MyProperty { get; set; } + + [UxmlTrait( "health2" , 8)] public int MyField; + [UxmlTrait( "health1" , 8)] public int MyField2{get; set; } + [UxmlTrait( "health1" , false)] public bool MyBoolField2; + [UxmlTrait( "health1" , "hi")] public string MyStringField2; + public void Test123() + { + Debug.Write( "test" ); + // Test987(); + } +} + +public abstract class AtVisualElement { } diff --git a/TestConsole/TestConsole.csproj b/TestConsole/TestConsole.csproj new file mode 100644 index 0000000..44ff54c --- /dev/null +++ b/TestConsole/TestConsole.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/TestGenerators.sln b/TestGenerators.sln new file mode 100644 index 0000000..403e2af --- /dev/null +++ b/TestGenerators.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleGenerator", "ExampleGenerator\ExampleGenerator.csproj", "{F9D135AA-6FFC-406C-B837-BCC9F97341FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsole", "TestConsole\TestConsole.csproj", "{4B37526B-5EE6-453B-BF68-3D5B9E9BB417}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F9D135AA-6FFC-406C-B837-BCC9F97341FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9D135AA-6FFC-406C-B837-BCC9F97341FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9D135AA-6FFC-406C-B837-BCC9F97341FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9D135AA-6FFC-406C-B837-BCC9F97341FF}.Release|Any CPU.Build.0 = Release|Any CPU + {4B37526B-5EE6-453B-BF68-3D5B9E9BB417}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B37526B-5EE6-453B-BF68-3D5B9E9BB417}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B37526B-5EE6-453B-BF68-3D5B9E9BB417}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B37526B-5EE6-453B-BF68-3D5B9E9BB417}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal