2022-02-27 20:57:30 +00:00
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using Microsoft.CodeAnalysis ;
2022-11-27 09:40:40 +00:00
using Microsoft.CodeAnalysis.CSharp ;
2022-02-27 20:57:30 +00:00
using Microsoft.CodeAnalysis.CSharp.Syntax ;
using Microsoft.CodeAnalysis.Text ;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptPropertyDefValGenerator : ISourceGenerator
{
public void Initialize ( GeneratorInitializationContext context )
{
}
public void Execute ( GeneratorExecutionContext context )
{
if ( context . AreGodotSourceGeneratorsDisabled ( ) )
return ;
INamedTypeSymbol [ ] godotClasses = context
. Compilation . SyntaxTrees
. SelectMany ( tree = >
tree . GetRoot ( ) . DescendantNodes ( )
. OfType < ClassDeclarationSyntax > ( )
. SelectGodotScriptClasses ( context . Compilation )
// Report and skip non-partial classes
. Where ( x = >
{
if ( x . cds . IsPartial ( ) )
{
if ( x . cds . IsNested ( ) & & ! x . cds . AreAllOuterTypesPartial ( out var typeMissingPartial ) )
{
Common . ReportNonPartialGodotScriptOuterClass ( context , typeMissingPartial ! ) ;
return false ;
}
return true ;
}
Common . ReportNonPartialGodotScriptClass ( context , x . cds , x . symbol ) ;
return false ;
} )
. Select ( x = > x . symbol )
)
. Distinct < INamedTypeSymbol > ( SymbolEqualityComparer . Default )
. ToArray ( ) ;
if ( godotClasses . Length > 0 )
{
2022-08-15 03:57:52 +00:00
var typeCache = new MarshalUtils . TypeCache ( context . Compilation ) ;
2022-02-27 20:57:30 +00:00
foreach ( var godotClass in godotClasses )
{
VisitGodotScriptClass ( context , typeCache , godotClass ) ;
}
}
}
private static void VisitGodotScriptClass (
GeneratorExecutionContext context ,
MarshalUtils . TypeCache typeCache ,
INamedTypeSymbol symbol
)
{
INamespaceSymbol namespaceSymbol = symbol . ContainingNamespace ;
string classNs = namespaceSymbol ! = null & & ! namespaceSymbol . IsGlobalNamespace ?
2022-11-24 00:04:15 +00:00
namespaceSymbol . FullQualifiedNameOmitGlobal ( ) :
2022-02-27 20:57:30 +00:00
string . Empty ;
bool hasNamespace = classNs . Length ! = 0 ;
bool isInnerClass = symbol . ContainingType ! = null ;
2022-11-24 00:04:15 +00:00
string uniqueHint = symbol . FullQualifiedNameOmitGlobal ( ) . SanitizeQualifiedNameForUniqueHint ( )
2022-10-22 21:13:52 +00:00
+ "_ScriptPropertyDefVal.generated" ;
2022-02-27 20:57:30 +00:00
var source = new StringBuilder ( ) ;
if ( hasNamespace )
{
source . Append ( "namespace " ) ;
source . Append ( classNs ) ;
source . Append ( " {\n\n" ) ;
}
if ( isInnerClass )
{
var containingType = symbol . ContainingType ;
while ( containingType ! = null )
{
source . Append ( "partial " ) ;
source . Append ( containingType . GetDeclarationKeyword ( ) ) ;
source . Append ( " " ) ;
source . Append ( containingType . NameWithTypeParameters ( ) ) ;
source . Append ( "\n{\n" ) ;
containingType = containingType . ContainingType ;
}
}
source . Append ( "partial class " ) ;
source . Append ( symbol . NameWithTypeParameters ( ) ) ;
source . Append ( "\n{\n" ) ;
var exportedMembers = new List < ExportedPropertyMetadata > ( ) ;
var members = symbol . GetMembers ( ) ;
var exportedProperties = members
. Where ( s = > ! s . IsStatic & & s . Kind = = SymbolKind . Property )
. Cast < IPropertySymbol > ( )
. Where ( s = > s . GetAttributes ( )
. Any ( a = > a . AttributeClass ? . IsGodotExportAttribute ( ) ? ? false ) )
. ToArray ( ) ;
var exportedFields = members
. Where ( s = > ! s . IsStatic & & s . Kind = = SymbolKind . Field & & ! s . IsImplicitlyDeclared )
. Cast < IFieldSymbol > ( )
. Where ( s = > s . GetAttributes ( )
. Any ( a = > a . AttributeClass ? . IsGodotExportAttribute ( ) ? ? false ) )
. ToArray ( ) ;
foreach ( var property in exportedProperties )
{
if ( property . IsStatic )
{
Common . ReportExportedMemberIsStatic ( context , property ) ;
continue ;
}
2022-08-28 16:16:57 +00:00
if ( property . IsIndexer )
{
Common . ReportExportedMemberIsIndexer ( context , property ) ;
continue ;
}
2022-02-27 20:57:30 +00:00
// TODO: We should still restore read-only properties after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload.
// Ignore properties without a getter or without a setter. Godot properties must be both readable and writable.
if ( property . IsWriteOnly )
{
Common . ReportExportedMemberIsWriteOnly ( context , property ) ;
continue ;
}
if ( property . IsReadOnly )
{
Common . ReportExportedMemberIsReadOnly ( context , property ) ;
continue ;
}
var propertyType = property . Type ;
var marshalType = MarshalUtils . ConvertManagedTypeToMarshalType ( propertyType , typeCache ) ;
if ( marshalType = = null )
{
Common . ReportExportedMemberTypeNotSupported ( context , property ) ;
continue ;
}
2022-11-27 09:40:40 +00:00
var propertyDeclarationSyntax = property . DeclaringSyntaxReferences
. Select ( r = > r . GetSyntax ( ) as PropertyDeclarationSyntax ) . FirstOrDefault ( ) ;
2022-02-27 20:57:30 +00:00
2022-11-12 03:34:43 +00:00
// Fully qualify the value to avoid issues with namespaces.
string? value = null ;
2022-11-27 09:40:40 +00:00
if ( propertyDeclarationSyntax ! = null )
2022-11-12 03:34:43 +00:00
{
2022-11-27 09:40:40 +00:00
if ( propertyDeclarationSyntax . Initializer ! = null )
{
var sm = context . Compilation . GetSemanticModel ( propertyDeclarationSyntax . Initializer . SyntaxTree ) ;
value = propertyDeclarationSyntax . Initializer . Value . FullQualifiedSyntax ( sm ) ;
}
else
{
2022-11-29 00:01:36 +00:00
var propertyGet = propertyDeclarationSyntax . AccessorList ? . Accessors
. Where ( a = > a . Keyword . IsKind ( SyntaxKind . GetKeyword ) ) . FirstOrDefault ( ) ;
2022-11-27 09:40:40 +00:00
if ( propertyGet ! = null )
{
if ( propertyGet . ExpressionBody ! = null )
{
if ( propertyGet . ExpressionBody . Expression is IdentifierNameSyntax identifierNameSyntax )
{
var sm = context . Compilation . GetSemanticModel ( identifierNameSyntax . SyntaxTree ) ;
var fieldSymbol = sm . GetSymbolInfo ( identifierNameSyntax ) . Symbol as IFieldSymbol ;
EqualsValueClauseSyntax ? initializer = fieldSymbol ? . DeclaringSyntaxReferences
. Select ( r = > r . GetSyntax ( ) )
. OfType < VariableDeclaratorSyntax > ( )
. Select ( s = > s . Initializer )
. FirstOrDefault ( i = > i ! = null ) ;
if ( initializer ! = null )
{
sm = context . Compilation . GetSemanticModel ( initializer . SyntaxTree ) ;
value = initializer . Value . FullQualifiedSyntax ( sm ) ;
}
}
}
else
{
var returns = propertyGet . DescendantNodes ( ) . OfType < ReturnStatementSyntax > ( ) ;
if ( returns . Count ( ) = = 1 )
2022-11-29 00:01:36 +00:00
{
// Generate only single return
2022-11-27 09:40:40 +00:00
var returnStatementSyntax = returns . Single ( ) ;
if ( returnStatementSyntax . Expression is IdentifierNameSyntax identifierNameSyntax )
{
var sm = context . Compilation . GetSemanticModel ( identifierNameSyntax . SyntaxTree ) ;
var fieldSymbol = sm . GetSymbolInfo ( identifierNameSyntax ) . Symbol as IFieldSymbol ;
EqualsValueClauseSyntax ? initializer = fieldSymbol ? . DeclaringSyntaxReferences
. Select ( r = > r . GetSyntax ( ) )
. OfType < VariableDeclaratorSyntax > ( )
. Select ( s = > s . Initializer )
. FirstOrDefault ( i = > i ! = null ) ;
if ( initializer ! = null )
{
sm = context . Compilation . GetSemanticModel ( initializer . SyntaxTree ) ;
value = initializer . Value . FullQualifiedSyntax ( sm ) ;
}
}
}
}
}
}
2022-11-12 03:34:43 +00:00
}
2022-02-27 20:57:30 +00:00
exportedMembers . Add ( new ExportedPropertyMetadata (
property . Name , marshalType . Value , propertyType , value ) ) ;
}
foreach ( var field in exportedFields )
{
if ( field . IsStatic )
{
Common . ReportExportedMemberIsStatic ( context , field ) ;
continue ;
}
// TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload.
// Ignore properties without a getter or without a setter. Godot properties must be both readable and writable.
if ( field . IsReadOnly )
{
Common . ReportExportedMemberIsReadOnly ( context , field ) ;
continue ;
}
var fieldType = field . Type ;
var marshalType = MarshalUtils . ConvertManagedTypeToMarshalType ( fieldType , typeCache ) ;
if ( marshalType = = null )
{
Common . ReportExportedMemberTypeNotSupported ( context , field ) ;
continue ;
}
EqualsValueClauseSyntax ? initializer = field . DeclaringSyntaxReferences
. Select ( r = > r . GetSyntax ( ) )
. OfType < VariableDeclaratorSyntax > ( )
. Select ( s = > s . Initializer )
. FirstOrDefault ( i = > i ! = null ) ;
2022-11-12 03:34:43 +00:00
// This needs to be fully qualified to avoid issues with namespaces.
string? value = null ;
if ( initializer ! = null )
{
var sm = context . Compilation . GetSemanticModel ( initializer . SyntaxTree ) ;
value = initializer . Value . FullQualifiedSyntax ( sm ) ;
}
2022-02-27 20:57:30 +00:00
exportedMembers . Add ( new ExportedPropertyMetadata (
field . Name , marshalType . Value , fieldType , value ) ) ;
}
// Generate GetGodotExportedProperties
if ( exportedMembers . Count > 0 )
{
source . Append ( "#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n" ) ;
2022-11-29 00:01:36 +00:00
string dictionaryType =
"global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>" ;
2022-02-27 20:57:30 +00:00
source . Append ( "#if TOOLS\n" ) ;
source . Append ( " internal new static " ) ;
source . Append ( dictionaryType ) ;
source . Append ( " GetGodotPropertyDefaultValues()\n {\n" ) ;
source . Append ( " var values = new " ) ;
source . Append ( dictionaryType ) ;
source . Append ( "(" ) ;
source . Append ( exportedMembers . Count ) ;
source . Append ( ");\n" ) ;
foreach ( var exportedMember in exportedMembers )
{
string defaultValueLocalName = string . Concat ( "__" , exportedMember . Name , "_default_value" ) ;
source . Append ( " " ) ;
2022-11-24 00:04:15 +00:00
source . Append ( exportedMember . TypeSymbol . FullQualifiedNameIncludeGlobal ( ) ) ;
2022-02-27 20:57:30 +00:00
source . Append ( " " ) ;
source . Append ( defaultValueLocalName ) ;
source . Append ( " = " ) ;
source . Append ( exportedMember . Value ? ? "default" ) ;
source . Append ( ";\n" ) ;
2022-09-06 12:43:40 +00:00
source . Append ( " values.Add(PropertyName." ) ;
2022-02-27 20:57:30 +00:00
source . Append ( exportedMember . Name ) ;
source . Append ( ", " ) ;
2022-12-01 00:45:11 +00:00
source . AppendManagedToVariantExpr ( defaultValueLocalName ,
exportedMember . TypeSymbol , exportedMember . Type ) ;
2022-02-27 20:57:30 +00:00
source . Append ( ");\n" ) ;
}
source . Append ( " return values;\n" ) ;
source . Append ( " }\n" ) ;
source . Append ( "#endif\n" ) ;
source . Append ( "#pragma warning restore CS0109\n" ) ;
}
source . Append ( "}\n" ) ; // partial class
if ( isInnerClass )
{
var containingType = symbol . ContainingType ;
while ( containingType ! = null )
{
source . Append ( "}\n" ) ; // outer class
containingType = containingType . ContainingType ;
}
}
if ( hasNamespace )
{
source . Append ( "\n}\n" ) ;
}
context . AddSource ( uniqueHint , SourceText . From ( source . ToString ( ) , Encoding . UTF8 ) ) ;
}
private struct ExportedPropertyMetadata
{
public ExportedPropertyMetadata ( string name , MarshalType type , ITypeSymbol typeSymbol , string? value )
{
Name = name ;
Type = type ;
TypeSymbol = typeSymbol ;
Value = value ;
}
public string Name { get ; }
public MarshalType Type { get ; }
public ITypeSymbol TypeSymbol { get ; }
public string? Value { get ; }
}
}
}