mirror of
				https://github.com/godotengine/godot.git
				synced 2025-11-01 06:01:14 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			458 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			458 lines
		
	
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System.Collections.Generic;
 | |
| using System.Linq;
 | |
| using System.Text;
 | |
| using Microsoft.CodeAnalysis;
 | |
| using Microsoft.CodeAnalysis.CSharp;
 | |
| 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.IsGodotSourceGeneratorDisabled("ScriptPropertyDefVal"))
 | |
|                 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 _))
 | |
|                                 {
 | |
|                                     return false;
 | |
|                                 }
 | |
| 
 | |
|                                 return true;
 | |
|                             }
 | |
| 
 | |
|                             return false;
 | |
|                         })
 | |
|                         .Select(x => x.symbol)
 | |
|                 )
 | |
|                 .Distinct<INamedTypeSymbol>(SymbolEqualityComparer.Default)
 | |
|                 .ToArray();
 | |
| 
 | |
|             if (godotClasses.Length > 0)
 | |
|             {
 | |
|                 var typeCache = new MarshalUtils.TypeCache(context.Compilation);
 | |
| 
 | |
|                 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 is { IsGlobalNamespace: false }
 | |
|                 ? namespaceSymbol.FullQualifiedNameOmitGlobal()
 | |
|                 : string.Empty;
 | |
|             bool hasNamespace = classNs.Length != 0;
 | |
| 
 | |
|             bool isNode = symbol.InheritsFrom("GodotSharp", GodotClasses.Node);
 | |
| 
 | |
|             bool isInnerClass = symbol.ContainingType != null;
 | |
| 
 | |
|             string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
 | |
|                                 + "_ScriptPropertyDefVal.generated";
 | |
| 
 | |
|             var source = new StringBuilder();
 | |
| 
 | |
|             if (hasNamespace)
 | |
|             {
 | |
|                 source.Append("namespace ");
 | |
|                 source.Append(classNs);
 | |
|                 source.Append(" {\n\n");
 | |
|             }
 | |
| 
 | |
|             if (isInnerClass)
 | |
|             {
 | |
|                 var containingType = symbol.ContainingType;
 | |
|                 AppendPartialContainingTypeDeclarations(containingType);
 | |
| 
 | |
|                 void AppendPartialContainingTypeDeclarations(INamedTypeSymbol? containingType)
 | |
|                 {
 | |
|                     if (containingType == null)
 | |
|                         return;
 | |
| 
 | |
|                     AppendPartialContainingTypeDeclarations(containingType.ContainingType);
 | |
| 
 | |
|                     source.Append("partial ");
 | |
|                     source.Append(containingType.GetDeclarationKeyword());
 | |
|                     source.Append(" ");
 | |
|                     source.Append(containingType.NameWithTypeParameters());
 | |
|                     source.Append("\n{\n");
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             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.Kind == SymbolKind.Property)
 | |
|                 .Cast<IPropertySymbol>()
 | |
|                 .Where(s => s.GetAttributes()
 | |
|                     .Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false))
 | |
|                 .ToArray();
 | |
| 
 | |
|             var exportedFields = members
 | |
|                 .Where(s => 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)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberIsStaticRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         property.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 if (property.IsIndexer)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberIsIndexerRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         property.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 // 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, without a setter or with an init-only setter.
 | |
|                 // Godot properties must be both readable and writable.
 | |
|                 if (property.IsWriteOnly)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedPropertyIsWriteOnlyRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         property.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 if (property.IsReadOnly || property.SetMethod!.IsInitOnly)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberIsReadOnlyRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         property.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 if (property.ExplicitInterfaceImplementations.Length > 0)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberIsExplicitInterfaceImplementationRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         property.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 var propertyType = property.Type;
 | |
|                 var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(propertyType, typeCache);
 | |
| 
 | |
|                 if (marshalType == null)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberTypeIsNotSupportedRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         property.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 if (!isNode && MemberHasNodeType(propertyType, marshalType.Value))
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.OnlyNodesShouldExportNodesRule,
 | |
|                         property.Locations.FirstLocationWithSourceTreeOrDefault()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 var propertyDeclarationSyntax = property.DeclaringSyntaxReferences
 | |
|                     .Select(r => r.GetSyntax() as PropertyDeclarationSyntax).FirstOrDefault();
 | |
| 
 | |
|                 // Fully qualify the value to avoid issues with namespaces.
 | |
|                 string? value = null;
 | |
|                 if (propertyDeclarationSyntax != null)
 | |
|                 {
 | |
|                     if (propertyDeclarationSyntax.Initializer != null)
 | |
|                     {
 | |
|                         var sm = context.Compilation.GetSemanticModel(propertyDeclarationSyntax.Initializer.SyntaxTree);
 | |
|                         value = propertyDeclarationSyntax.Initializer.Value.FullQualifiedSyntax(sm);
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         var propertyGet = propertyDeclarationSyntax.AccessorList?.Accessors
 | |
|                             .FirstOrDefault(a => a.Keyword.IsKind(SyntaxKind.GetKeyword));
 | |
|                         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)
 | |
|                                 {
 | |
|                                     // Generate only single return
 | |
|                                     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);
 | |
|                                         }
 | |
|                                     }
 | |
|                                 }
 | |
|                             }
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 exportedMembers.Add(new ExportedPropertyMetadata(
 | |
|                     property.Name, marshalType.Value, propertyType, value));
 | |
|             }
 | |
| 
 | |
|             foreach (var field in exportedFields)
 | |
|             {
 | |
|                 if (field.IsStatic)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberIsStaticRule,
 | |
|                         field.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         field.ToDisplayString()
 | |
|                     ));
 | |
|                     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)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberIsReadOnlyRule,
 | |
|                         field.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         field.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 var fieldType = field.Type;
 | |
|                 var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(fieldType, typeCache);
 | |
| 
 | |
|                 if (marshalType == null)
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.ExportedMemberTypeIsNotSupportedRule,
 | |
|                         field.Locations.FirstLocationWithSourceTreeOrDefault(),
 | |
|                         field.ToDisplayString()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 if (!isNode && MemberHasNodeType(fieldType, marshalType.Value))
 | |
|                 {
 | |
|                     context.ReportDiagnostic(Diagnostic.Create(
 | |
|                         Common.OnlyNodesShouldExportNodesRule,
 | |
|                         field.Locations.FirstLocationWithSourceTreeOrDefault()
 | |
|                     ));
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 EqualsValueClauseSyntax? initializer = field.DeclaringSyntaxReferences
 | |
|                     .Select(r => r.GetSyntax())
 | |
|                     .OfType<VariableDeclaratorSyntax>()
 | |
|                     .Select(s => s.Initializer)
 | |
|                     .FirstOrDefault(i => i != null);
 | |
| 
 | |
|                 // 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);
 | |
|                 }
 | |
| 
 | |
|                 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");
 | |
| 
 | |
|                 const string DictionaryType =
 | |
|                     "global::System.Collections.Generic.Dictionary<global::Godot.StringName, global::Godot.Variant>";
 | |
| 
 | |
|                 source.Append("#if TOOLS\n");
 | |
| 
 | |
|                 source.Append("    /// <summary>\n")
 | |
|                     .Append("    /// Get the default values for all properties declared in this class.\n")
 | |
|                     .Append("    /// This method is used by Godot to determine the value that will be\n")
 | |
|                     .Append("    /// used by the inspector when resetting properties.\n")
 | |
|                     .Append("    /// Do not call this method.\n")
 | |
|                     .Append("    /// </summary>\n");
 | |
| 
 | |
|                 source.Append("    [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\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("        ");
 | |
|                     source.Append(exportedMember.TypeSymbol.FullQualifiedNameIncludeGlobal());
 | |
|                     source.Append(" ");
 | |
|                     source.Append(defaultValueLocalName);
 | |
|                     source.Append(" = ");
 | |
|                     source.Append(exportedMember.Value ?? "default");
 | |
|                     source.Append(";\n");
 | |
|                     source.Append("        values.Add(PropertyName.@");
 | |
|                     source.Append(exportedMember.Name);
 | |
|                     source.Append(", ");
 | |
|                     source.AppendManagedToVariantExpr(defaultValueLocalName,
 | |
|                         exportedMember.TypeSymbol, exportedMember.Type);
 | |
|                     source.Append(");\n");
 | |
|                 }
 | |
| 
 | |
|                 source.Append("        return values;\n");
 | |
|                 source.Append("    }\n");
 | |
| 
 | |
|                 source.Append("#endif // TOOLS\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 static bool MemberHasNodeType(ITypeSymbol memberType, MarshalType marshalType)
 | |
|         {
 | |
|             if (marshalType == MarshalType.GodotObjectOrDerived)
 | |
|             {
 | |
|                 return memberType.InheritsFrom("GodotSharp", GodotClasses.Node);
 | |
|             }
 | |
|             if (marshalType == MarshalType.GodotObjectOrDerivedArray)
 | |
|             {
 | |
|                 var elementType = ((IArrayTypeSymbol)memberType).ElementType;
 | |
|                 return elementType.InheritsFrom("GodotSharp", GodotClasses.Node);
 | |
|             }
 | |
|             if (memberType is INamedTypeSymbol { IsGenericType: true } genericType)
 | |
|             {
 | |
|                 return genericType.TypeArguments
 | |
|                     .Any(static typeArgument
 | |
|                         => typeArgument.InheritsFrom("GodotSharp", GodotClasses.Node));
 | |
|             }
 | |
| 
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         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; }
 | |
|         }
 | |
|     }
 | |
| }
 | 
