initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled

This commit is contained in:
2025-09-16 20:46:46 -04:00
commit 9d30169a8d
13378 changed files with 7050105 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
## Release 4.0
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
GD0001 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0001.html)
GD0002 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0002.html)
GD0101 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0101.html)
GD0102 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0102.html)
GD0103 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0103.html)
GD0104 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0104.html)
GD0105 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0105.html)
GD0106 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0106.html)
GD0201 | Usage | Error | ScriptSignalsGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0201.html)
GD0202 | Usage | Error | ScriptSignalsGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0202.html)
GD0203 | Usage | Error | ScriptSignalsGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0203.html)
GD0301 | Usage | Error | MustBeVariantAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0301.html)
GD0302 | Usage | Error | MustBeVariantAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0302.html)
GD0303 | Usage | Error | MustBeVariantAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0303.html)
## Release 4.2
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
GD0107 | Usage | Error | ScriptPropertyDefValGenerator, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0107.html)
GD0401 | Usage | Error | GlobalClassAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0401.html)
GD0402 | Usage | Error | GlobalClassAnalyzer, [Documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/diagnostics/GD0402.html)
## Release 4.3
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
GD0003 | Usage | Error | ScriptPathAttributeGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0003.html)
## Release 4.4
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
GD0108 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0108.html)
GD0109 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0109.html)
GD0110 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0110.html)
GD0111 | Usage | Error | ScriptPropertiesGenerator, [Documentation](https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/diagnostics/GD0111.html)

View File

@@ -0,0 +1,112 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ClassPartialModifierAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
ImmutableArray.Create(Common.ClassPartialModifierRule, Common.OuterClassPartialModifierRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
}
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
if (context.Node is not ClassDeclarationSyntax classDeclaration)
return;
if (context.ContainingSymbol is not INamedTypeSymbol typeSymbol)
return;
if (!typeSymbol.InheritsFrom("GodotSharp", GodotClasses.GodotObject))
return;
if (!classDeclaration.IsPartial())
context.ReportDiagnostic(Diagnostic.Create(
Common.ClassPartialModifierRule,
classDeclaration.Identifier.GetLocation(),
typeSymbol.ToDisplayString()));
var outerClassDeclaration = context.Node.Parent as ClassDeclarationSyntax;
while (outerClassDeclaration is not null)
{
var outerClassTypeSymbol = context.SemanticModel.GetDeclaredSymbol(outerClassDeclaration);
if (outerClassTypeSymbol == null)
return;
if (!outerClassDeclaration.IsPartial())
context.ReportDiagnostic(Diagnostic.Create(
Common.OuterClassPartialModifierRule,
outerClassDeclaration.Identifier.GetLocation(),
outerClassTypeSymbol.ToDisplayString()));
outerClassDeclaration = outerClassDeclaration.Parent as ClassDeclarationSyntax;
}
}
}
[ExportCodeFixProvider(LanguageNames.CSharp)]
public sealed class ClassPartialModifierCodeFixProvider : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(Common.ClassPartialModifierRule.Id);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
// Get the syntax root of the document.
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
// Get the diagnostic to fix.
var diagnostic = context.Diagnostics.First();
// Get the location of code issue.
var diagnosticSpan = diagnostic.Location.SourceSpan;
// Use that location to find the containing class declaration.
var classDeclaration = root?.FindToken(diagnosticSpan.Start)
.Parent?
.AncestorsAndSelf()
.OfType<ClassDeclarationSyntax>()
.First();
if (classDeclaration == null)
return;
context.RegisterCodeFix(
CodeAction.Create(
"Add partial modifier",
cancellationToken => AddPartialModifierAsync(context.Document, classDeclaration, cancellationToken),
classDeclaration.ToFullString()),
context.Diagnostics);
}
private static async Task<Document> AddPartialModifierAsync(Document document,
ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken)
{
// Create a new partial modifier.
var partialModifier = SyntaxFactory.Token(SyntaxKind.PartialKeyword);
var modifiedClassDeclaration = classDeclaration.AddModifiers(partialModifier);
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
// Replace the old class declaration with the modified one in the syntax root.
var newRoot = root!.ReplaceNode(classDeclaration, modifiedClassDeclaration);
var newDocument = document.WithSyntaxRoot(newRoot);
return newDocument;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
public sealed class NotNullAttribute : Attribute
{
}
}

View File

@@ -0,0 +1,230 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Godot.SourceGenerators
{
public static partial class Common
{
private static readonly string _helpLinkFormat = $"{VersionDocsUrl}/tutorials/scripting/c_sharp/diagnostics/{{0}}.html";
internal static readonly DiagnosticDescriptor ClassPartialModifierRule =
new DiagnosticDescriptor(id: "GD0001",
title: $"Missing partial modifier on declaration of type that derives from '{GodotClasses.GodotObject}'",
messageFormat: $"Missing partial modifier on declaration of type '{{0}}' that derives from '{GodotClasses.GodotObject}'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
$"Classes that derive from '{GodotClasses.GodotObject}' must be declared with the partial modifier.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0001"));
internal static readonly DiagnosticDescriptor OuterClassPartialModifierRule =
new DiagnosticDescriptor(id: "GD0002",
title: $"Missing partial modifier on declaration of type which contains nested classes that derive from '{GodotClasses.GodotObject}'",
messageFormat: $"Missing partial modifier on declaration of type '{{0}}' which contains nested classes that derive from '{GodotClasses.GodotObject}'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
$"Classes that derive from '{GodotClasses.GodotObject}' and their containing types must be declared with the partial modifier.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0002"));
public static readonly DiagnosticDescriptor MultipleClassesInGodotScriptRule =
new DiagnosticDescriptor(id: "GD0003",
title: "Found multiple classes with the same name in the same script file",
messageFormat: "Found multiple classes with the name '{0}' in the same script file",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"Found multiple classes with the same name in the same script file. A script file must only contain one class with a name that matches the file name.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0003"));
public static readonly DiagnosticDescriptor ExportedMemberIsStaticRule =
new DiagnosticDescriptor(id: "GD0101",
title: "The exported member is static",
messageFormat: "The exported member '{0}' is static",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported member is static. Only instance fields and properties can be exported. Remove the 'static' modifier, or the '[Export]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0101"));
public static readonly DiagnosticDescriptor ExportedMemberTypeIsNotSupportedRule =
new DiagnosticDescriptor(id: "GD0102",
title: "The type of the exported member is not supported",
messageFormat: "The type of the exported member '{0}' is not supported",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The type of the exported member is not supported. Use a supported type, or remove the '[Export]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0102"));
public static readonly DiagnosticDescriptor ExportedMemberIsReadOnlyRule =
new DiagnosticDescriptor(id: "GD0103",
title: "The exported member is read-only",
messageFormat: "The exported member '{0}' is read-only",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported member is read-only. Exported member must be writable.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0103"));
public static readonly DiagnosticDescriptor ExportedPropertyIsWriteOnlyRule =
new DiagnosticDescriptor(id: "GD0104",
title: "The exported property is write-only",
messageFormat: "The exported property '{0}' is write-only",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported property is write-only. Exported properties must be readable.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0104"));
public static readonly DiagnosticDescriptor ExportedMemberIsIndexerRule =
new DiagnosticDescriptor(id: "GD0105",
title: "The exported property is an indexer",
messageFormat: "The exported property '{0}' is an indexer",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported property is an indexer. Remove the '[Export]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0105"));
public static readonly DiagnosticDescriptor ExportedMemberIsExplicitInterfaceImplementationRule =
new DiagnosticDescriptor(id: "GD0106",
title: "The exported property is an explicit interface implementation",
messageFormat: "The exported property '{0}' is an explicit interface implementation",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported property is an explicit interface implementation. Remove the '[Export]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0106"));
public static readonly DiagnosticDescriptor OnlyNodesShouldExportNodesRule =
new DiagnosticDescriptor(id: "GD0107",
title: "Types not derived from Node should not export Node members",
messageFormat: "Types not derived from Node should not export Node members",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"Types not derived from Node should not export Node members. Node export is only supported in Node-derived classes.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0107"));
public static readonly DiagnosticDescriptor OnlyToolClassesShouldUseExportToolButtonRule =
new DiagnosticDescriptor(id: "GD0108",
title: "The exported tool button is not in a tool class",
messageFormat: "The exported tool button '{0}' is not in a tool class",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported tool button is not in a tool class. Annotate the class with the '[Tool]' attribute, or remove the '[ExportToolButton]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0108"));
public static readonly DiagnosticDescriptor ExportToolButtonShouldNotBeUsedWithExportRule =
new DiagnosticDescriptor(id: "GD0109",
title: "The '[ExportToolButton]' attribute cannot be used with another '[Export]' attribute",
messageFormat: "The '[ExportToolButton]' attribute cannot be used with another '[Export]' attribute on '{0}'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The '[ExportToolButton]' attribute cannot be used with the '[Export]' attribute. Remove one of the attributes.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0109"));
public static readonly DiagnosticDescriptor ExportToolButtonIsNotCallableRule =
new DiagnosticDescriptor(id: "GD0110",
title: "The exported tool button is not a Callable",
messageFormat: "The exported tool button '{0}' is not a Callable",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported tool button is not a Callable. The '[ExportToolButton]' attribute is only supported on members of type Callable.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0110"));
public static readonly DiagnosticDescriptor ExportToolButtonMustBeExpressionBodiedProperty =
new DiagnosticDescriptor(id: "GD0111",
title: "The exported tool button must be an expression-bodied property",
messageFormat: "The exported tool button '{0}' must be an expression-bodied property",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The exported tool button must be an expression-bodied property. The '[ExportToolButton]' attribute is only supported on expression-bodied properties with a 'new Callable(...)' or 'Callable.From(...)' expression.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0111"));
public static readonly DiagnosticDescriptor SignalDelegateMissingSuffixRule =
new DiagnosticDescriptor(id: "GD0201",
title: "The name of the delegate must end with 'EventHandler'",
messageFormat: "The name of the delegate '{0}' must end with 'EventHandler'",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The name of the delegate must end with 'EventHandler'. Rename the delegate accordingly, or remove the '[Signal]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0201"));
public static readonly DiagnosticDescriptor SignalParameterTypeNotSupportedRule =
new DiagnosticDescriptor(id: "GD0202",
title: "The parameter of the delegate signature of the signal is not supported",
messageFormat: "The parameter of the delegate signature of the signal '{0}' is not supported",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The parameter of the delegate signature of the signal is not supported. Use supported types only, or remove the '[Signal]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0202"));
public static readonly DiagnosticDescriptor SignalDelegateSignatureMustReturnVoidRule =
new DiagnosticDescriptor(id: "GD0203",
title: "The delegate signature of the signal must return void",
messageFormat: "The delegate signature of the signal '{0}' must return void",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The delegate signature of the signal must return void. Return void, or remove the '[Signal]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0203"));
public static readonly DiagnosticDescriptor GenericTypeArgumentMustBeVariantRule =
new DiagnosticDescriptor(id: "GD0301",
title: "The generic type argument must be a Variant compatible type",
messageFormat: "The generic type argument '{0}' must be a Variant compatible type",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The generic type argument must be a Variant compatible type. Use a Variant compatible type as the generic type argument.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0301"));
public static readonly DiagnosticDescriptor GenericTypeParameterMustBeVariantAnnotatedRule =
new DiagnosticDescriptor(id: "GD0302",
title: "The generic type parameter must be annotated with the '[MustBeVariant]' attribute",
messageFormat: "The generic type parameter '{0}' must be annotated with the '[MustBeVariant]' attribute",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The generic type parameter must be annotated with the '[MustBeVariant]' attribute. Add the '[MustBeVariant]' attribute to the generic type parameter.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0302"));
public static readonly DiagnosticDescriptor TypeArgumentParentSymbolUnhandledRule =
new DiagnosticDescriptor(id: "GD0303",
title: "The parent symbol of a type argument that must be Variant compatible was not handled",
messageFormat: "The parent symbol '{0}' of a type argument that must be Variant compatible was not handled",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The parent symbol of a type argument that must be Variant compatible was not handled. This is an issue in the engine, and should be reported.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0303"));
public static readonly DiagnosticDescriptor GlobalClassMustDeriveFromGodotObjectRule =
new DiagnosticDescriptor(id: "GD0401",
title: $"The class must derive from {GodotClasses.GodotObject} or a derived class",
messageFormat: $"The class '{{0}}' must derive from {GodotClasses.GodotObject} or a derived class",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
$"The class must derive from {GodotClasses.GodotObject} or a derived class. Change the base type, or remove the '[GlobalClass]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0401"));
public static readonly DiagnosticDescriptor GlobalClassMustNotBeGenericRule =
new DiagnosticDescriptor(id: "GD0402",
title: "The class must not be generic",
messageFormat: "The class '{0}' must not be generic",
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
"The class must not be generic. Make the class non-generic, or remove the '[GlobalClass]' attribute.",
helpLinkUri: string.Format(_helpLinkFormat, "GD0402"));
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class EventHandlerSuffixSuppressor : DiagnosticSuppressor
{
private static readonly SuppressionDescriptor _descriptor = new(
id: "GDSP0001",
suppressedDiagnosticId: "CA1711",
justification: "Signal delegates are used in events so the naming follows the guidelines.");
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions =>
ImmutableArray.Create(_descriptor);
public override void ReportSuppressions(SuppressionAnalysisContext context)
{
foreach (var diagnostic in context.ReportedDiagnostics)
{
AnalyzeDiagnostic(context, diagnostic, context.CancellationToken);
}
}
private static void AnalyzeDiagnostic(SuppressionAnalysisContext context, Diagnostic diagnostic, CancellationToken cancellationToken = default)
{
var location = diagnostic.Location;
var root = location.SourceTree?.GetRoot(cancellationToken);
var dds = root?
.FindNode(location.SourceSpan)
.DescendantNodesAndSelf()
.OfType<DelegateDeclarationSyntax>()
.FirstOrDefault();
if (dds == null)
return;
var semanticModel = context.GetSemanticModel(dds.SyntaxTree);
var delegateSymbol = semanticModel.GetDeclaredSymbol(dds, cancellationToken);
if (delegateSymbol == null)
return;
if (delegateSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false))
{
context.ReportSuppression(Suppression.Create(_descriptor, diagnostic));
}
}
}
}

View File

@@ -0,0 +1,389 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Godot.SourceGenerators
{
internal static class ExtensionMethods
{
public static bool TryGetGlobalAnalyzerProperty(
this GeneratorExecutionContext context, string property, out string? value
) => context.AnalyzerConfigOptions.GlobalOptions
.TryGetValue("build_property." + property, out value);
public static bool AreGodotSourceGeneratorsDisabled(this GeneratorExecutionContext context)
=> context.TryGetGlobalAnalyzerProperty("GodotSourceGenerators", out string? toggle) &&
toggle != null &&
toggle.Equals("disabled", StringComparison.OrdinalIgnoreCase);
public static bool IsGodotToolsProject(this GeneratorExecutionContext context)
=> context.TryGetGlobalAnalyzerProperty("IsGodotToolsProject", out string? toggle) &&
toggle != null &&
toggle.Equals("true", StringComparison.OrdinalIgnoreCase);
public static bool IsGodotSourceGeneratorDisabled(this GeneratorExecutionContext context, string generatorName) =>
AreGodotSourceGeneratorsDisabled(context) ||
(context.TryGetGlobalAnalyzerProperty("GodotDisabledSourceGenerators", out string? disabledGenerators) &&
disabledGenerators != null &&
disabledGenerators.Split(';').Contains(generatorName));
public static bool InheritsFrom(this ITypeSymbol? symbol, string assemblyName, string typeFullName)
{
while (symbol != null)
{
if (symbol.ContainingAssembly?.Name == assemblyName &&
symbol.FullQualifiedNameOmitGlobal() == typeFullName)
{
return true;
}
symbol = symbol.BaseType;
}
return false;
}
public static INamedTypeSymbol? GetGodotScriptNativeClass(this INamedTypeSymbol classTypeSymbol)
{
var symbol = classTypeSymbol;
while (symbol != null)
{
if (symbol.ContainingAssembly?.Name == "GodotSharp")
return symbol;
symbol = symbol.BaseType;
}
return null;
}
public static string? GetGodotScriptNativeClassName(this INamedTypeSymbol classTypeSymbol)
{
var nativeType = classTypeSymbol.GetGodotScriptNativeClass();
if (nativeType == null)
return null;
var godotClassNameAttr = nativeType.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.IsGodotClassNameAttribute() ?? false);
string? godotClassName = null;
if (godotClassNameAttr is { ConstructorArguments: { Length: > 0 } })
godotClassName = godotClassNameAttr.ConstructorArguments[0].Value?.ToString();
return godotClassName ?? nativeType.Name;
}
private static bool TryGetGodotScriptClass(
this ClassDeclarationSyntax cds, Compilation compilation,
out INamedTypeSymbol? symbol
)
{
var sm = compilation.GetSemanticModel(cds.SyntaxTree);
var classTypeSymbol = sm.GetDeclaredSymbol(cds);
if (classTypeSymbol?.BaseType == null
|| !classTypeSymbol.BaseType.InheritsFrom("GodotSharp", GodotClasses.GodotObject))
{
symbol = null;
return false;
}
symbol = classTypeSymbol;
return true;
}
public static IEnumerable<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol)> SelectGodotScriptClasses(
this IEnumerable<ClassDeclarationSyntax> source,
Compilation compilation
)
{
foreach (var cds in source)
{
if (cds.TryGetGodotScriptClass(compilation, out var symbol))
yield return (cds, symbol!);
}
}
public static bool IsNested(this TypeDeclarationSyntax cds)
=> cds.Parent is TypeDeclarationSyntax;
public static bool IsPartial(this TypeDeclarationSyntax cds)
=> cds.Modifiers.Any(SyntaxKind.PartialKeyword);
public static bool AreAllOuterTypesPartial(
this TypeDeclarationSyntax cds,
out TypeDeclarationSyntax? typeMissingPartial
)
{
SyntaxNode? outerSyntaxNode = cds.Parent;
while (outerSyntaxNode is TypeDeclarationSyntax outerTypeDeclSyntax)
{
if (!outerTypeDeclSyntax.IsPartial())
{
typeMissingPartial = outerTypeDeclSyntax;
return false;
}
outerSyntaxNode = outerSyntaxNode.Parent;
}
typeMissingPartial = null;
return true;
}
public static string GetDeclarationKeyword(this INamedTypeSymbol namedTypeSymbol)
{
string? keyword = namedTypeSymbol.DeclaringSyntaxReferences
.OfType<TypeDeclarationSyntax>().FirstOrDefault()?
.Keyword.Text;
return keyword ?? namedTypeSymbol.TypeKind switch
{
TypeKind.Interface => "interface",
TypeKind.Struct => "struct",
_ => "class"
};
}
public static string GetAccessibilityKeyword(this INamedTypeSymbol namedTypeSymbol)
{
if (namedTypeSymbol.DeclaredAccessibility == Accessibility.NotApplicable)
{
// Accessibility not specified. Get the default accessibility.
return namedTypeSymbol.ContainingSymbol switch
{
null or INamespaceSymbol => "internal",
ITypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } => "private",
ITypeSymbol { TypeKind: TypeKind.Interface } => "public",
_ => "",
};
}
return namedTypeSymbol.DeclaredAccessibility switch
{
Accessibility.Private => "private",
Accessibility.Protected => "protected",
Accessibility.Internal => "internal",
Accessibility.ProtectedAndInternal => "private",
Accessibility.ProtectedOrInternal => "private",
Accessibility.Public => "public",
_ => "",
};
}
private static SymbolDisplayFormat FullyQualifiedFormatOmitGlobal { get; } =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted);
private static SymbolDisplayFormat FullyQualifiedFormatIncludeGlobal { get; } =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Included);
public static string FullQualifiedNameOmitGlobal(this ITypeSymbol symbol)
=> symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal);
public static string FullQualifiedNameOmitGlobal(this INamespaceSymbol namespaceSymbol)
=> namespaceSymbol.ToDisplayString(FullyQualifiedFormatOmitGlobal);
public static string FullQualifiedNameIncludeGlobal(this ITypeSymbol symbol)
=> symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatIncludeGlobal);
public static string FullQualifiedNameIncludeGlobal(this INamespaceSymbol namespaceSymbol)
=> namespaceSymbol.ToDisplayString(FullyQualifiedFormatIncludeGlobal);
public static string FullQualifiedSyntax(this SyntaxNode node, SemanticModel sm)
{
StringBuilder sb = new();
FullQualifiedSyntax(node, sm, sb, true);
return sb.ToString();
}
private static void FullQualifiedSyntax(SyntaxNode node, SemanticModel sm, StringBuilder sb, bool isFirstNode)
{
if (node is NameSyntax ns && isFirstNode)
{
SymbolInfo nameInfo = sm.GetSymbolInfo(ns);
sb.Append(nameInfo.Symbol?.ToDisplayString(FullyQualifiedFormatIncludeGlobal) ?? ns.ToString());
return;
}
bool innerIsFirstNode = true;
foreach (var child in node.ChildNodesAndTokens())
{
if (child.HasLeadingTrivia)
{
sb.Append(child.GetLeadingTrivia());
}
if (child.IsNode)
{
var childNode = child.AsNode()!;
if (node is InterpolationSyntax && childNode is ExpressionSyntax)
{
ParenEnclosedFullQualifiedSyntax(childNode, sm, sb, isFirstNode: innerIsFirstNode);
}
else
{
FullQualifiedSyntax(childNode, sm, sb, isFirstNode: innerIsFirstNode);
}
innerIsFirstNode = false;
}
else
{
sb.Append(child);
}
if (child.HasTrailingTrivia)
{
sb.Append(child.GetTrailingTrivia());
}
}
static void ParenEnclosedFullQualifiedSyntax(SyntaxNode node, SemanticModel sm, StringBuilder sb, bool isFirstNode)
{
sb.Append(SyntaxFactory.Token(SyntaxKind.OpenParenToken));
FullQualifiedSyntax(node, sm, sb, isFirstNode);
sb.Append(SyntaxFactory.Token(SyntaxKind.CloseParenToken));
}
}
public static string SanitizeQualifiedNameForUniqueHint(this string qualifiedName)
=> qualifiedName
// AddSource() doesn't support @ prefix
.Replace("@", "")
// AddSource() doesn't support angle brackets
.Replace("<", "(Of ")
.Replace(">", ")");
public static bool IsGodotExportAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ExportAttr;
public static bool IsGodotSignalAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.SignalAttr;
public static bool IsGodotMustBeVariantAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.MustBeVariantAttr;
public static bool IsGodotClassNameAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.GodotClassNameAttr;
public static bool IsGodotGlobalClassAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.GlobalClassAttr;
public static bool IsGodotExportToolButtonAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ExportToolButtonAttr;
public static bool IsGodotToolAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.ToolAttr;
public static bool IsSystemFlagsAttribute(this INamedTypeSymbol symbol)
=> symbol.FullQualifiedNameOmitGlobal() == GodotClasses.SystemFlagsAttr;
public static GodotMethodData? HasGodotCompatibleSignature(
this IMethodSymbol method,
MarshalUtils.TypeCache typeCache
)
{
if (method.IsGenericMethod)
return null;
var retSymbol = method.ReturnType;
var retType = method.ReturnsVoid ?
null :
MarshalUtils.ConvertManagedTypeToMarshalType(method.ReturnType, typeCache);
if (retType == null && !method.ReturnsVoid)
return null;
var parameters = method.Parameters;
var paramTypes = parameters
// Currently we don't support `ref`, `out`, `in`, `ref readonly` parameters (and we never may)
.Where(p => p.RefKind == RefKind.None)
// Attempt to determine the variant type
.Select(p => MarshalUtils.ConvertManagedTypeToMarshalType(p.Type, typeCache))
// Discard parameter types that couldn't be determined (null entries)
.Where(t => t != null).Cast<MarshalType>().ToImmutableArray();
// If any parameter type was incompatible, it was discarded so the length won't match
if (parameters.Length > paramTypes.Length)
return null; // Ignore incompatible method
return new GodotMethodData(method, paramTypes,
parameters.Select(p => p.Type).ToImmutableArray(),
retType != null ? (retType.Value, retSymbol) : null);
}
public static IEnumerable<GodotMethodData> WhereHasGodotCompatibleSignature(
this IEnumerable<IMethodSymbol> methods,
MarshalUtils.TypeCache typeCache
)
{
foreach (var method in methods)
{
var methodData = HasGodotCompatibleSignature(method, typeCache);
if (methodData != null)
yield return methodData.Value;
}
}
public static IEnumerable<GodotPropertyData> WhereIsGodotCompatibleType(
this IEnumerable<IPropertySymbol> properties,
MarshalUtils.TypeCache typeCache
)
{
foreach (var property in properties)
{
var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(property.Type, typeCache);
if (marshalType == null)
continue;
yield return new GodotPropertyData(property, marshalType.Value);
}
}
public static IEnumerable<GodotFieldData> WhereIsGodotCompatibleType(
this IEnumerable<IFieldSymbol> fields,
MarshalUtils.TypeCache typeCache
)
{
foreach (var field in fields)
{
// TODO: We should still restore read-only fields after reloading assembly. Two possible ways: reflection or turn RestoreGodotObjectData into a constructor overload.
var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(field.Type, typeCache);
if (marshalType == null)
continue;
yield return new GodotFieldData(field, marshalType.Value);
}
}
public static Location? FirstLocationWithSourceTreeOrDefault(this IEnumerable<Location> locations)
{
return locations.FirstOrDefault(location => location.SourceTree != null) ?? locations.FirstOrDefault();
}
public static string Path(this Location location)
=> location.SourceTree?.GetLineSpan(location.SourceSpan).Path
?? location.GetLineSpan().Path;
public static int StartLine(this Location location)
=> location.SourceTree?.GetLineSpan(location.SourceSpan).StartLinePosition.Line
?? location.GetLineSpan().StartLinePosition.Line;
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class GlobalClassAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(
Common.GlobalClassMustDeriveFromGodotObjectRule,
Common.GlobalClassMustNotBeGenericRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration);
}
private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
// Return if not a type symbol or the type is not a global class.
if (context.ContainingSymbol is not INamedTypeSymbol typeSymbol ||
!typeSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotGlobalClassAttribute() ?? false))
return;
if (typeSymbol.IsGenericType)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.GlobalClassMustNotBeGenericRule,
typeSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
typeSymbol.ToDisplayString()
));
}
if (!typeSymbol.InheritsFrom("GodotSharp", GodotClasses.GodotObject))
{
context.ReportDiagnostic(Diagnostic.Create(
Common.GlobalClassMustDeriveFromGodotObjectRule,
typeSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
typeSymbol.ToDisplayString()
));
}
}
}
}

View File

@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<PropertyGroup>
<Description>Core C# source generator for Godot projects.</Description>
<Authors>Godot Engine contributors</Authors>
<PackageId>Godot.SourceGenerators</PackageId>
<Version>4.5.0</Version>
<PackageVersion>$(PackageVersion_Godot_SourceGenerators)</PackageVersion>
<RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk/Godot.SourceGenerators</RepositoryUrl>
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Copyright>Copyright (c) Godot Engine contributors</Copyright>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- Do not include the generator as a lib dependency -->
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<!-- Analyzer release tracking -->
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.11.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<!-- Package the props file -->
<None Include="Godot.SourceGenerators.props" Pack="true" PackagePath="build" Visible="true" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,10 @@
<Project>
<ItemGroup>
<!-- $(GodotProjectDir) is defined by Godot.NET.Sdk -->
<CompilerVisibleProperty Include="GodotDisabledSourceGenerators" />
<CompilerVisibleProperty Include="GodotProjectDir" />
<CompilerVisibleProperty Include="GodotProjectDirBase64" />
<CompilerVisibleProperty Include="GodotSourceGenerators" />
<CompilerVisibleProperty Include="IsGodotToolsProject" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
namespace Godot.SourceGenerators
{
public static class GodotClasses
{
public const string GodotObject = "Godot.GodotObject";
public const string Node = "Godot.Node";
public const string Callable = "Godot.Callable";
public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute";
public const string ExportAttr = "Godot.ExportAttribute";
public const string ExportCategoryAttr = "Godot.ExportCategoryAttribute";
public const string ExportGroupAttr = "Godot.ExportGroupAttribute";
public const string ExportSubgroupAttr = "Godot.ExportSubgroupAttribute";
public const string ExportToolButtonAttr = "Godot.ExportToolButtonAttribute";
public const string SignalAttr = "Godot.SignalAttribute";
public const string MustBeVariantAttr = "Godot.MustBeVariantAttribute";
public const string GodotClassNameAttr = "Godot.GodotClassNameAttribute";
public const string GlobalClassAttr = "Godot.GlobalClassAttribute";
public const string ToolAttr = "Godot.ToolAttribute";
public const string SystemFlagsAttr = "System.FlagsAttribute";
}
}

View File

@@ -0,0 +1,144 @@
using System;
namespace Godot.SourceGenerators
{
// TODO: May need to think about compatibility here. Could Godot change these values between minor versions?
internal enum VariantType
{
Nil = 0,
Bool = 1,
Int = 2,
Float = 3,
String = 4,
Vector2 = 5,
Vector2I = 6,
Rect2 = 7,
Rect2I = 8,
Vector3 = 9,
Vector3I = 10,
Transform2D = 11,
Vector4 = 12,
Vector4I = 13,
Plane = 14,
Quaternion = 15,
Aabb = 16,
Basis = 17,
Transform3D = 18,
Projection = 19,
Color = 20,
StringName = 21,
NodePath = 22,
Rid = 23,
Object = 24,
Callable = 25,
Signal = 26,
Dictionary = 27,
Array = 28,
PackedByteArray = 29,
PackedInt32Array = 30,
PackedInt64Array = 31,
PackedFloat32Array = 32,
PackedFloat64Array = 33,
PackedStringArray = 34,
PackedVector2Array = 35,
PackedVector3Array = 36,
PackedColorArray = 37,
PackedVector4Array = 38,
Max = 39
}
internal enum PropertyHint
{
None = 0,
Range = 1,
Enum = 2,
EnumSuggestion = 3,
ExpEasing = 4,
Link = 5,
Flags = 6,
Layers2DRender = 7,
Layers2DPhysics = 8,
Layers2DNavigation = 9,
Layers3DRender = 10,
Layers3DPhysics = 11,
Layers3DNavigation = 12,
File = 13,
Dir = 14,
GlobalFile = 15,
GlobalDir = 16,
ResourceType = 17,
MultilineText = 18,
Expression = 19,
PlaceholderText = 20,
ColorNoAlpha = 21,
ObjectId = 22,
TypeString = 23,
NodePathToEditedNode = 24,
ObjectTooBig = 25,
NodePathValidTypes = 26,
SaveFile = 27,
GlobalSaveFile = 28,
IntIsObjectid = 29,
IntIsPointer = 30,
ArrayType = 31,
LocaleId = 32,
LocalizableString = 33,
NodeType = 34,
HideQuaternionEdit = 35,
Password = 36,
LayersAvoidance = 37,
DictionaryType = 38,
ToolButton = 39,
Max = 40
}
[Flags]
internal enum PropertyUsageFlags
{
None = 0,
Storage = 2,
Editor = 4,
Internal = 8,
Checkable = 16,
Checked = 32,
Group = 64,
Category = 128,
Subgroup = 256,
ClassIsBitfield = 512,
NoInstanceState = 1024,
RestartIfChanged = 2048,
ScriptVariable = 4096,
StoreIfNull = 8192,
UpdateAllIfModified = 16384,
ScriptDefaultValue = 32768,
ClassIsEnum = 65536,
NilIsVariant = 131072,
Array = 262144,
AlwaysDuplicate = 524288,
NeverDuplicate = 1048576,
HighEndGfx = 2097152,
NodePathFromSceneRoot = 4194304,
ResourceNotPersistent = 8388608,
KeyingIncrements = 16777216,
DeferredSetResource = 33554432,
EditorInstantiateObject = 67108864,
EditorBasicSetting = 134217728,
ReadOnly = 268435456,
Default = 6,
NoEditor = 2
}
[Flags]
public enum MethodFlags
{
Normal = 1,
Editor = 2,
Const = 4,
Virtual = 8,
Vararg = 16,
Static = 32,
ObjectCore = 64,
Default = 1
}
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
namespace Godot.SourceGenerators
{
public readonly struct GodotMethodData
{
public GodotMethodData(IMethodSymbol method, ImmutableArray<MarshalType> paramTypes,
ImmutableArray<ITypeSymbol> paramTypeSymbols, (MarshalType MarshalType, ITypeSymbol TypeSymbol)? retType)
{
Method = method;
ParamTypes = paramTypes;
ParamTypeSymbols = paramTypeSymbols;
RetType = retType;
}
public IMethodSymbol Method { get; }
public ImmutableArray<MarshalType> ParamTypes { get; }
public ImmutableArray<ITypeSymbol> ParamTypeSymbols { get; }
public (MarshalType MarshalType, ITypeSymbol TypeSymbol)? RetType { get; }
}
public readonly struct GodotSignalDelegateData
{
public GodotSignalDelegateData(string name, INamedTypeSymbol delegateSymbol, GodotMethodData invokeMethodData)
{
Name = name;
DelegateSymbol = delegateSymbol;
InvokeMethodData = invokeMethodData;
}
public string Name { get; }
public INamedTypeSymbol DelegateSymbol { get; }
public GodotMethodData InvokeMethodData { get; }
}
public readonly struct GodotPropertyData
{
public GodotPropertyData(IPropertySymbol propertySymbol, MarshalType type)
{
PropertySymbol = propertySymbol;
Type = type;
}
public IPropertySymbol PropertySymbol { get; }
public MarshalType Type { get; }
}
public readonly struct GodotFieldData
{
public GodotFieldData(IFieldSymbol fieldSymbol, MarshalType type)
{
FieldSymbol = fieldSymbol;
Type = type;
}
public IFieldSymbol FieldSymbol { get; }
public MarshalType Type { get; }
}
public struct GodotPropertyOrFieldData
{
public GodotPropertyOrFieldData(ISymbol symbol, MarshalType type)
{
Symbol = symbol;
Type = type;
}
public GodotPropertyOrFieldData(GodotPropertyData propertyData)
: this(propertyData.PropertySymbol, propertyData.Type)
{
}
public GodotPropertyOrFieldData(GodotFieldData fieldData)
: this(fieldData.FieldSymbol, fieldData.Type)
{
}
public ISymbol Symbol { get; }
public MarshalType Type { get; }
}
}

View File

@@ -0,0 +1,63 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class GodotPluginsInitializerGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
if (context.IsGodotToolsProject() || context.IsGodotSourceGeneratorDisabled("GodotPluginsInitializer"))
return;
string source =
@"using System;
using System.Runtime.InteropServices;
using Godot.Bridge;
using Godot.NativeInterop;
namespace GodotPlugins.Game
{
internal static partial class Main
{
[UnmanagedCallersOnly(EntryPoint = ""godotsharp_game_main_init"")]
private static godot_bool InitializeFromGameProject(IntPtr godotDllHandle, IntPtr outManagedCallbacks,
IntPtr unmanagedCallbacks, int unmanagedCallbacksSize)
{
try
{
DllImportResolver dllImportResolver = new GodotDllImportResolver(godotDllHandle).OnResolveDllImport;
var coreApiAssembly = typeof(global::Godot.GodotObject).Assembly;
NativeLibrary.SetDllImportResolver(coreApiAssembly, dllImportResolver);
NativeFuncs.Initialize(unmanagedCallbacks, unmanagedCallbacksSize);
ManagedCallbacks.Create(outManagedCallbacks);
ScriptManagerBridge.LookupScriptsInAssembly(typeof(global::GodotPlugins.Game.Main).Assembly);
return godot_bool.True;
}
catch (Exception e)
{
global::System.Console.Error.WriteLine(e);
return false.ToGodotBool();
}
}
}
}
";
context.AddSource("GodotPlugins.Game.generated",
SourceText.From(source, Encoding.UTF8));
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Godot.SourceGenerators
{
public static class Helper
{
[Conditional("DEBUG")]
public static void ThrowIfNull([NotNull] object? value)
{
_ = value ?? throw new ArgumentNullException();
}
}
}

View File

@@ -0,0 +1,74 @@
using System.Diagnostics.CodeAnalysis;
namespace Godot.SourceGenerators
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum MarshalType
{
Boolean,
Char,
SByte,
Int16,
Int32,
Int64,
Byte,
UInt16,
UInt32,
UInt64,
Single,
Double,
String,
// Godot structs
Vector2,
Vector2I,
Rect2,
Rect2I,
Transform2D,
Vector3,
Vector3I,
Basis,
Quaternion,
Transform3D,
Vector4,
Vector4I,
Projection,
Aabb,
Color,
Plane,
Callable,
Signal,
// Enums
Enum,
// Arrays
ByteArray,
Int32Array,
Int64Array,
Float32Array,
Float64Array,
StringArray,
Vector2Array,
Vector3Array,
Vector4Array,
ColorArray,
GodotObjectOrDerivedArray,
SystemArrayOfStringName,
SystemArrayOfNodePath,
SystemArrayOfRid,
// Variant
Variant,
// Classes
GodotObjectOrDerived,
StringName,
NodePath,
Rid,
GodotDictionary,
GodotArray,
GodotGenericDictionary,
GodotGenericArray,
}
}

View File

@@ -0,0 +1,394 @@
using System;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace Godot.SourceGenerators
{
internal static class MarshalUtils
{
public class TypeCache
{
public INamedTypeSymbol GodotObjectType { get; }
public TypeCache(Compilation compilation)
{
INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName)
{
return compilation.GetTypeByMetadataName(fullyQualifiedMetadataName) ??
throw new InvalidOperationException($"Type not found: '{fullyQualifiedMetadataName}'.");
}
GodotObjectType = GetTypeByMetadataNameOrThrow(GodotClasses.GodotObject);
}
}
public static VariantType? ConvertMarshalTypeToVariantType(MarshalType marshalType)
=> marshalType switch
{
MarshalType.Boolean => VariantType.Bool,
MarshalType.Char => VariantType.Int,
MarshalType.SByte => VariantType.Int,
MarshalType.Int16 => VariantType.Int,
MarshalType.Int32 => VariantType.Int,
MarshalType.Int64 => VariantType.Int,
MarshalType.Byte => VariantType.Int,
MarshalType.UInt16 => VariantType.Int,
MarshalType.UInt32 => VariantType.Int,
MarshalType.UInt64 => VariantType.Int,
MarshalType.Single => VariantType.Float,
MarshalType.Double => VariantType.Float,
MarshalType.String => VariantType.String,
MarshalType.Vector2 => VariantType.Vector2,
MarshalType.Vector2I => VariantType.Vector2I,
MarshalType.Rect2 => VariantType.Rect2,
MarshalType.Rect2I => VariantType.Rect2I,
MarshalType.Transform2D => VariantType.Transform2D,
MarshalType.Vector3 => VariantType.Vector3,
MarshalType.Vector3I => VariantType.Vector3I,
MarshalType.Basis => VariantType.Basis,
MarshalType.Quaternion => VariantType.Quaternion,
MarshalType.Transform3D => VariantType.Transform3D,
MarshalType.Vector4 => VariantType.Vector4,
MarshalType.Vector4I => VariantType.Vector4I,
MarshalType.Projection => VariantType.Projection,
MarshalType.Aabb => VariantType.Aabb,
MarshalType.Color => VariantType.Color,
MarshalType.Plane => VariantType.Plane,
MarshalType.Callable => VariantType.Callable,
MarshalType.Signal => VariantType.Signal,
MarshalType.Enum => VariantType.Int,
MarshalType.ByteArray => VariantType.PackedByteArray,
MarshalType.Int32Array => VariantType.PackedInt32Array,
MarshalType.Int64Array => VariantType.PackedInt64Array,
MarshalType.Float32Array => VariantType.PackedFloat32Array,
MarshalType.Float64Array => VariantType.PackedFloat64Array,
MarshalType.StringArray => VariantType.PackedStringArray,
MarshalType.Vector2Array => VariantType.PackedVector2Array,
MarshalType.Vector3Array => VariantType.PackedVector3Array,
MarshalType.Vector4Array => VariantType.PackedVector4Array,
MarshalType.ColorArray => VariantType.PackedColorArray,
MarshalType.GodotObjectOrDerivedArray => VariantType.Array,
MarshalType.SystemArrayOfStringName => VariantType.Array,
MarshalType.SystemArrayOfNodePath => VariantType.Array,
MarshalType.SystemArrayOfRid => VariantType.Array,
MarshalType.Variant => VariantType.Nil,
MarshalType.GodotObjectOrDerived => VariantType.Object,
MarshalType.StringName => VariantType.StringName,
MarshalType.NodePath => VariantType.NodePath,
MarshalType.Rid => VariantType.Rid,
MarshalType.GodotDictionary => VariantType.Dictionary,
MarshalType.GodotArray => VariantType.Array,
MarshalType.GodotGenericDictionary => VariantType.Dictionary,
MarshalType.GodotGenericArray => VariantType.Array,
_ => null
};
public static MarshalType? ConvertManagedTypeToMarshalType(ITypeSymbol type, TypeCache typeCache)
{
var specialType = type.SpecialType;
switch (specialType)
{
case SpecialType.System_Boolean:
return MarshalType.Boolean;
case SpecialType.System_Char:
return MarshalType.Char;
case SpecialType.System_SByte:
return MarshalType.SByte;
case SpecialType.System_Int16:
return MarshalType.Int16;
case SpecialType.System_Int32:
return MarshalType.Int32;
case SpecialType.System_Int64:
return MarshalType.Int64;
case SpecialType.System_Byte:
return MarshalType.Byte;
case SpecialType.System_UInt16:
return MarshalType.UInt16;
case SpecialType.System_UInt32:
return MarshalType.UInt32;
case SpecialType.System_UInt64:
return MarshalType.UInt64;
case SpecialType.System_Single:
return MarshalType.Single;
case SpecialType.System_Double:
return MarshalType.Double;
case SpecialType.System_String:
return MarshalType.String;
default:
{
var typeKind = type.TypeKind;
if (typeKind == TypeKind.Enum)
return MarshalType.Enum;
if (typeKind == TypeKind.Struct)
{
if (type.ContainingAssembly?.Name == "GodotSharp" &&
type.ContainingNamespace?.Name == "Godot")
{
return type switch
{
{ Name: "Vector2" } => MarshalType.Vector2,
{ Name: "Vector2I" } => MarshalType.Vector2I,
{ Name: "Rect2" } => MarshalType.Rect2,
{ Name: "Rect2I" } => MarshalType.Rect2I,
{ Name: "Transform2D" } => MarshalType.Transform2D,
{ Name: "Vector3" } => MarshalType.Vector3,
{ Name: "Vector3I" } => MarshalType.Vector3I,
{ Name: "Basis" } => MarshalType.Basis,
{ Name: "Quaternion" } => MarshalType.Quaternion,
{ Name: "Transform3D" } => MarshalType.Transform3D,
{ Name: "Vector4" } => MarshalType.Vector4,
{ Name: "Vector4I" } => MarshalType.Vector4I,
{ Name: "Projection" } => MarshalType.Projection,
{ Name: "Aabb" } => MarshalType.Aabb,
{ Name: "Color" } => MarshalType.Color,
{ Name: "Plane" } => MarshalType.Plane,
{ Name: "Rid" } => MarshalType.Rid,
{ Name: "Callable" } => MarshalType.Callable,
{ Name: "Signal" } => MarshalType.Signal,
{ Name: "Variant" } => MarshalType.Variant,
_ => null
};
}
}
else if (typeKind == TypeKind.Array)
{
var arrayType = (IArrayTypeSymbol)type;
if (arrayType.Rank != 1)
return null;
var elementType = arrayType.ElementType;
switch (elementType.SpecialType)
{
case SpecialType.System_Byte:
return MarshalType.ByteArray;
case SpecialType.System_Int32:
return MarshalType.Int32Array;
case SpecialType.System_Int64:
return MarshalType.Int64Array;
case SpecialType.System_Single:
return MarshalType.Float32Array;
case SpecialType.System_Double:
return MarshalType.Float64Array;
case SpecialType.System_String:
return MarshalType.StringArray;
}
if (elementType.SimpleDerivesFrom(typeCache.GodotObjectType))
return MarshalType.GodotObjectOrDerivedArray;
if (elementType.ContainingAssembly?.Name == "GodotSharp" &&
elementType.ContainingNamespace?.Name == "Godot")
{
switch (elementType)
{
case { Name: "Vector2" }:
return MarshalType.Vector2Array;
case { Name: "Vector3" }:
return MarshalType.Vector3Array;
case { Name: "Vector4" }:
return MarshalType.Vector4Array;
case { Name: "Color" }:
return MarshalType.ColorArray;
case { Name: "StringName" }:
return MarshalType.SystemArrayOfStringName;
case { Name: "NodePath" }:
return MarshalType.SystemArrayOfNodePath;
case { Name: "Rid" }:
return MarshalType.SystemArrayOfRid;
}
}
return null;
}
else
{
if (type.SimpleDerivesFrom(typeCache.GodotObjectType))
return MarshalType.GodotObjectOrDerived;
if (type.ContainingAssembly?.Name == "GodotSharp")
{
switch (type.ContainingNamespace?.Name)
{
case "Godot":
return type switch
{
{ Name: "StringName" } => MarshalType.StringName,
{ Name: "NodePath" } => MarshalType.NodePath,
_ => null
};
case "Collections"
when type.ContainingNamespace?.FullQualifiedNameOmitGlobal() == "Godot.Collections":
return type switch
{
{ Name: "Dictionary" } =>
type is INamedTypeSymbol { IsGenericType: false } ?
MarshalType.GodotDictionary :
MarshalType.GodotGenericDictionary,
{ Name: "Array" } =>
type is INamedTypeSymbol { IsGenericType: false } ?
MarshalType.GodotArray :
MarshalType.GodotGenericArray,
_ => null
};
}
}
}
break;
}
}
return null;
}
private static bool SimpleDerivesFrom(this ITypeSymbol? type, ITypeSymbol candidateBaseType)
{
while (type != null)
{
if (SymbolEqualityComparer.Default.Equals(type, candidateBaseType))
return true;
type = type.BaseType;
}
return false;
}
public static ITypeSymbol? GetArrayElementType(ITypeSymbol typeSymbol)
{
if (typeSymbol.TypeKind == TypeKind.Array)
{
var arrayType = (IArrayTypeSymbol)typeSymbol;
return arrayType.ElementType;
}
if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType)
return genericType.TypeArguments.FirstOrDefault();
return null;
}
public static ITypeSymbol[]? GetGenericElementTypes(ITypeSymbol typeSymbol)
{
if (typeSymbol is INamedTypeSymbol { IsGenericType: true } genericType)
return genericType.TypeArguments.ToArray();
return null;
}
private static StringBuilder Append(this StringBuilder source, string a, string b)
=> source.Append(a).Append(b);
private static StringBuilder Append(this StringBuilder source, string a, string b, string c)
=> source.Append(a).Append(b).Append(c);
private static StringBuilder Append(this StringBuilder source, string a, string b,
string c, string d)
=> source.Append(a).Append(b).Append(c).Append(d);
private static StringBuilder Append(this StringBuilder source, string a, string b,
string c, string d, string e)
=> source.Append(a).Append(b).Append(c).Append(d).Append(e);
private static StringBuilder Append(this StringBuilder source, string a, string b,
string c, string d, string e, string f)
=> source.Append(a).Append(b).Append(c).Append(d).Append(e).Append(f);
private static StringBuilder Append(this StringBuilder source, string a, string b,
string c, string d, string e, string f, string g)
=> source.Append(a).Append(b).Append(c).Append(d).Append(e).Append(f).Append(g);
private static StringBuilder Append(this StringBuilder source, string a, string b,
string c, string d, string e, string f, string g, string h)
=> source.Append(a).Append(b).Append(c).Append(d).Append(e).Append(f).Append(g).Append(h);
private const string VariantUtils = "global::Godot.NativeInterop.VariantUtils";
public static StringBuilder AppendNativeVariantToManagedExpr(this StringBuilder source,
string inputExpr, ITypeSymbol typeSymbol, MarshalType marshalType)
{
return marshalType switch
{
// We need a special case for GodotObjectOrDerived[], because it's not supported by VariantUtils.ConvertTo<T>
MarshalType.GodotObjectOrDerivedArray =>
source.Append(VariantUtils, ".ConvertToSystemArrayOfGodotObject<",
((IArrayTypeSymbol)typeSymbol).ElementType.FullQualifiedNameIncludeGlobal(), ">(",
inputExpr, ")"),
// We need a special case for generic Godot collections and GodotObjectOrDerived[], because VariantUtils.ConvertTo<T> is slower
MarshalType.GodotGenericDictionary =>
source.Append(VariantUtils, ".ConvertToDictionary<",
((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ", ",
((INamedTypeSymbol)typeSymbol).TypeArguments[1].FullQualifiedNameIncludeGlobal(), ">(",
inputExpr, ")"),
MarshalType.GodotGenericArray =>
source.Append(VariantUtils, ".ConvertToArray<",
((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ">(",
inputExpr, ")"),
_ => source.Append(VariantUtils, ".ConvertTo<",
typeSymbol.FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")"),
};
}
public static StringBuilder AppendManagedToNativeVariantExpr(this StringBuilder source,
string inputExpr, ITypeSymbol typeSymbol, MarshalType marshalType)
{
return marshalType switch
{
// We need a special case for GodotObjectOrDerived[], because it's not supported by VariantUtils.CreateFrom<T>
MarshalType.GodotObjectOrDerivedArray =>
source.Append(VariantUtils, ".CreateFromSystemArrayOfGodotObject(", inputExpr, ")"),
// We need a special case for generic Godot collections and GodotObjectOrDerived[], because VariantUtils.CreateFrom<T> is slower
MarshalType.GodotGenericDictionary =>
source.Append(VariantUtils, ".CreateFromDictionary(", inputExpr, ")"),
MarshalType.GodotGenericArray =>
source.Append(VariantUtils, ".CreateFromArray(", inputExpr, ")"),
_ => source.Append(VariantUtils, ".CreateFrom<",
typeSymbol.FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")"),
};
}
public static StringBuilder AppendVariantToManagedExpr(this StringBuilder source,
string inputExpr, ITypeSymbol typeSymbol, MarshalType marshalType)
{
return marshalType switch
{
// We need a special case for GodotObjectOrDerived[], because it's not supported by Variant.As<T>
MarshalType.GodotObjectOrDerivedArray =>
source.Append(inputExpr, ".AsGodotObjectArray<",
((IArrayTypeSymbol)typeSymbol).ElementType.FullQualifiedNameIncludeGlobal(), ">()"),
// We need a special case for generic Godot collections and GodotObjectOrDerived[], because Variant.As<T> is slower
MarshalType.GodotGenericDictionary =>
source.Append(inputExpr, ".AsGodotDictionary<",
((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ", ",
((INamedTypeSymbol)typeSymbol).TypeArguments[1].FullQualifiedNameIncludeGlobal(), ">()"),
MarshalType.GodotGenericArray =>
source.Append(inputExpr, ".AsGodotArray<",
((INamedTypeSymbol)typeSymbol).TypeArguments[0].FullQualifiedNameIncludeGlobal(), ">()"),
_ => source.Append(inputExpr, ".As<",
typeSymbol.FullQualifiedNameIncludeGlobal(), ">()")
};
}
public static StringBuilder AppendManagedToVariantExpr(this StringBuilder source,
string inputExpr, ITypeSymbol typeSymbol, MarshalType marshalType)
{
return marshalType switch
{
// We need a special case for GodotObjectOrDerived[], because it's not supported by Variant.From<T>
MarshalType.GodotObjectOrDerivedArray =>
source.Append("global::Godot.Variant.CreateFrom(", inputExpr, ")"),
// We need a special case for generic Godot collections, because Variant.From<T> is slower
MarshalType.GodotGenericDictionary or MarshalType.GodotGenericArray =>
source.Append("global::Godot.Variant.CreateFrom(", inputExpr, ")"),
_ => source.Append("global::Godot.Variant.From<",
typeSymbol.FullQualifiedNameIncludeGlobal(), ">(", inputExpr, ")")
};
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace Godot.SourceGenerators
{
internal readonly struct MethodInfo
{
public MethodInfo(string name, PropertyInfo returnVal, MethodFlags flags,
List<PropertyInfo>? arguments,
List<string?>? defaultArguments)
{
Name = name;
ReturnVal = returnVal;
Flags = flags;
Arguments = arguments;
DefaultArguments = defaultArguments;
}
public string Name { get; }
public PropertyInfo ReturnVal { get; }
public MethodFlags Flags { get; }
public List<PropertyInfo>? Arguments { get; }
public List<string?>? DefaultArguments { get; }
}
}

View File

@@ -0,0 +1,167 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Godot.SourceGenerators
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class MustBeVariantAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
=> ImmutableArray.Create(
Common.GenericTypeArgumentMustBeVariantRule,
Common.GenericTypeParameterMustBeVariantAnnotatedRule,
Common.TypeArgumentParentSymbolUnhandledRule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.TypeArgumentList);
}
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
// Ignore syntax inside comments
if (IsInsideDocumentation(context.Node))
return;
var typeArgListSyntax = (TypeArgumentListSyntax)context.Node;
// Method invocation or variable declaration that contained the type arguments
var parentSyntax = context.Node.Parent;
Helper.ThrowIfNull(parentSyntax);
var sm = context.SemanticModel;
var typeCache = new MarshalUtils.TypeCache(context.Compilation);
for (int i = 0; i < typeArgListSyntax.Arguments.Count; i++)
{
var typeSyntax = typeArgListSyntax.Arguments[i];
// Ignore omitted type arguments, e.g.: List<>, Dictionary<,>, etc
if (typeSyntax is OmittedTypeArgumentSyntax)
continue;
var typeSymbol = sm.GetSymbolInfo(typeSyntax).Symbol as ITypeSymbol;
Helper.ThrowIfNull(typeSymbol);
var parentSymbolInfo = sm.GetSymbolInfo(parentSyntax);
var parentSymbol = parentSymbolInfo.Symbol;
if (parentSymbol == null)
{
if (parentSymbolInfo.CandidateReason == CandidateReason.LateBound)
{
// Invocations on dynamic are late bound so we can't retrieve the symbol.
continue;
}
Helper.ThrowIfNull(parentSymbol);
}
if (!ShouldCheckTypeArgument(context, parentSyntax, parentSymbol, typeSyntax, typeSymbol, i))
{
return;
}
if (typeSymbol is ITypeParameterSymbol typeParamSymbol)
{
if (!typeParamSymbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false))
{
context.ReportDiagnostic(Diagnostic.Create(
Common.GenericTypeParameterMustBeVariantAnnotatedRule,
typeSyntax.GetLocation(),
typeSymbol.ToDisplayString()
));
}
continue;
}
var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(typeSymbol, typeCache);
if (marshalType is null)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.GenericTypeArgumentMustBeVariantRule,
typeSyntax.GetLocation(),
typeSymbol.ToDisplayString()
));
}
}
}
/// <summary>
/// Check if the syntax node is inside a documentation syntax.
/// </summary>
/// <param name="syntax">Syntax node to check.</param>
/// <returns><see langword="true"/> if the syntax node is inside a documentation syntax.</returns>
private bool IsInsideDocumentation(SyntaxNode? syntax)
{
while (syntax != null)
{
if (syntax is DocumentationCommentTriviaSyntax)
{
return true;
}
syntax = syntax.Parent;
}
return false;
}
/// <summary>
/// Check if the given type argument is being used in a type parameter that contains
/// the <c>MustBeVariantAttribute</c>; otherwise, we ignore the attribute.
/// </summary>
/// <param name="context">Context for a syntax node action.</param>
/// <param name="parentSyntax">The parent node syntax that contains the type node syntax.</param>
/// <param name="parentSymbol">The symbol retrieved for the parent node syntax.</param>
/// <param name="typeArgumentSyntax">The type node syntax of the argument type to check.</param>
/// <param name="typeArgumentSymbol">The symbol retrieved for the type node syntax.</param>
/// <param name="typeArgumentIndex"></param>
/// <returns><see langword="true"/> if the type must be variant and must be analyzed.</returns>
private bool ShouldCheckTypeArgument(
SyntaxNodeAnalysisContext context,
SyntaxNode parentSyntax,
ISymbol parentSymbol,
TypeSyntax typeArgumentSyntax,
ITypeSymbol typeArgumentSymbol,
int typeArgumentIndex)
{
ITypeParameterSymbol? typeParamSymbol = parentSymbol switch
{
IMethodSymbol methodSymbol when parentSyntax.Ancestors().Any(s => s is AttributeSyntax) &&
methodSymbol.ContainingType.TypeParameters.Length > 0
=> methodSymbol.ContainingType.TypeParameters[typeArgumentIndex],
IMethodSymbol { TypeParameters.Length: > 0 } methodSymbol
=> methodSymbol.TypeParameters[typeArgumentIndex],
INamedTypeSymbol { TypeParameters.Length: > 0 } typeSymbol
=> typeSymbol.TypeParameters[typeArgumentIndex],
_
=> null
};
if (typeParamSymbol != null)
{
return typeParamSymbol.GetAttributes()
.Any(a => a.AttributeClass?.IsGodotMustBeVariantAttribute() ?? false);
}
context.ReportDiagnostic(Diagnostic.Create(
Common.TypeArgumentParentSymbolUnhandledRule,
typeArgumentSyntax.GetLocation(),
parentSymbol.ToDisplayString()
));
return false;
}
}
}

View File

@@ -0,0 +1,29 @@
namespace Godot.SourceGenerators
{
internal readonly struct PropertyInfo
{
public PropertyInfo(VariantType type, string name, PropertyHint hint,
string? hintString, PropertyUsageFlags usage, bool exported)
: this(type, name, hint, hintString, usage, className: null, exported) { }
public PropertyInfo(VariantType type, string name, PropertyHint hint,
string? hintString, PropertyUsageFlags usage, string? className, bool exported)
{
Type = type;
Name = name;
Hint = hint;
HintString = hintString;
Usage = usage;
ClassName = className;
Exported = exported;
}
public VariantType Type { get; }
public string Name { get; }
public PropertyHint Hint { get; }
public string? HintString { get; }
public PropertyUsageFlags Usage { get; }
public string? ClassName { get; }
public bool Exported { get; }
}
}

View File

@@ -0,0 +1,474 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptMethodsGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
if (context.IsGodotSourceGeneratorDisabled("ScriptMethods"))
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 class MethodOverloadEqualityComparer : IEqualityComparer<GodotMethodData>
{
public bool Equals(GodotMethodData x, GodotMethodData y)
=> x.ParamTypes.Length == y.ParamTypes.Length && x.Method.Name == y.Method.Name;
public int GetHashCode(GodotMethodData obj)
{
unchecked
{
return (obj.ParamTypes.Length.GetHashCode() * 397) ^ obj.Method.Name.GetHashCode();
}
}
}
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
MarshalUtils.TypeCache typeCache,
INamedTypeSymbol symbol
)
{
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedNameOmitGlobal() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null;
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
+ "_ScriptMethods.generated";
var source = new StringBuilder();
source.Append("using Godot;\n");
source.Append("using Godot.NativeInterop;\n");
source.Append("\n");
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.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
}
}
source.Append("partial class ");
source.Append(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
var members = symbol.GetMembers();
var methodSymbols = members
.Where(s => s.Kind == SymbolKind.Method && !s.IsImplicitlyDeclared)
.Cast<IMethodSymbol>()
.Where(m => m.MethodKind == MethodKind.Ordinary);
var godotClassMethods = methodSymbols.WhereHasGodotCompatibleSignature(typeCache)
.Distinct(new MethodOverloadEqualityComparer())
.ToArray();
source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
source.Append(" /// <summary>\n")
.Append(" /// Cached StringNames for the methods contained in this class, for fast lookup.\n")
.Append(" /// </summary>\n");
source.Append(
$" public new class MethodName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.MethodName {{\n");
// Generate cached StringNames for methods and properties, for fast lookup
var distinctMethodNames = godotClassMethods
.Select(m => m.Method.Name)
.Distinct()
.ToArray();
foreach (string methodName in distinctMethodNames)
{
source.Append(" /// <summary>\n")
.Append(" /// Cached name for the '")
.Append(methodName)
.Append("' method.\n")
.Append(" /// </summary>\n");
source.Append(" public new static readonly global::Godot.StringName @");
source.Append(methodName);
source.Append(" = \"");
source.Append(methodName);
source.Append("\";\n");
}
source.Append(" }\n"); // class GodotInternal
// Generate GetGodotMethodList
if (godotClassMethods.Length > 0)
{
const string ListType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>";
source.Append(" /// <summary>\n")
.Append(" /// Get the method information for all the methods declared in this class.\n")
.Append(" /// This method is used by Godot to register the available methods in the editor.\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 ")
.Append(ListType)
.Append(" GetGodotMethodList()\n {\n");
source.Append(" var methods = new ")
.Append(ListType)
.Append("(")
.Append(godotClassMethods.Length)
.Append(");\n");
foreach (var method in godotClassMethods)
{
var methodInfo = DetermineMethodInfo(method);
AppendMethodInfo(source, methodInfo);
}
source.Append(" return methods;\n");
source.Append(" }\n");
}
source.Append("#pragma warning restore CS0109\n");
// Generate InvokeGodotClassMethod
if (godotClassMethods.Length > 0)
{
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(" protected override bool InvokeGodotClassMethod(in godot_string_name method, ");
source.Append("NativeVariantPtrArgs args, out godot_variant ret)\n {\n");
foreach (var method in godotClassMethods)
{
GenerateMethodInvoker(method, source);
}
source.Append(" return base.InvokeGodotClassMethod(method, args, out ret);\n");
source.Append(" }\n");
}
// Generate InvokeGodotClassStaticMethod
var godotClassStaticMethods = godotClassMethods.Where(m => m.Method.IsStatic).ToArray();
if (godotClassStaticMethods.Length > 0)
{
source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(" internal new static bool InvokeGodotClassStaticMethod(in godot_string_name method, ");
source.Append("NativeVariantPtrArgs args, out godot_variant ret)\n {\n");
foreach (var method in godotClassStaticMethods)
{
GenerateMethodInvoker(method, source);
}
source.Append(" ret = default;\n");
source.Append(" return false;\n");
source.Append(" }\n");
source.Append("#pragma warning restore CS0109\n");
}
// Generate HasGodotClassMethod
if (distinctMethodNames.Length > 0)
{
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(" protected override bool HasGodotClassMethod(in godot_string_name method)\n {\n");
foreach (string methodName in distinctMethodNames)
{
GenerateHasMethodEntry(methodName, source);
}
source.Append(" return base.HasGodotClassMethod(method);\n");
source.Append(" }\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 void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo)
{
source.Append(" methods.Add(new(name: MethodName.@")
.Append(methodInfo.Name)
.Append(", returnVal: ");
AppendPropertyInfo(source, methodInfo.ReturnVal);
source.Append(", flags: (global::Godot.MethodFlags)")
.Append((int)methodInfo.Flags)
.Append(", arguments: ");
if (methodInfo.Arguments is { Count: > 0 })
{
source.Append("new() { ");
foreach (var param in methodInfo.Arguments)
{
AppendPropertyInfo(source, param);
// C# allows colon after the last element
source.Append(", ");
}
source.Append(" }");
}
else
{
source.Append("null");
}
source.Append(", defaultArguments: null));\n");
}
private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
{
source.Append("new(type: (global::Godot.Variant.Type)")
.Append((int)propertyInfo.Type)
.Append(", name: \"")
.Append(propertyInfo.Name)
.Append("\", hint: (global::Godot.PropertyHint)")
.Append((int)propertyInfo.Hint)
.Append(", hintString: \"")
.Append(propertyInfo.HintString)
.Append("\", usage: (global::Godot.PropertyUsageFlags)")
.Append((int)propertyInfo.Usage)
.Append(", exported: ")
.Append(propertyInfo.Exported ? "true" : "false");
if (propertyInfo.ClassName != null)
{
source.Append(", className: new global::Godot.StringName(\"")
.Append(propertyInfo.ClassName)
.Append("\")");
}
source.Append(")");
}
private static MethodInfo DetermineMethodInfo(GodotMethodData method)
{
PropertyInfo returnVal;
if (method.RetType != null)
{
returnVal = DeterminePropertyInfo(method.RetType.Value.MarshalType,
method.RetType.Value.TypeSymbol,
name: string.Empty);
}
else
{
returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None,
hintString: null, PropertyUsageFlags.Default, exported: false);
}
int paramCount = method.ParamTypes.Length;
List<PropertyInfo>? arguments;
if (paramCount > 0)
{
arguments = new(capacity: paramCount);
for (int i = 0; i < paramCount; i++)
{
arguments.Add(DeterminePropertyInfo(method.ParamTypes[i],
method.Method.Parameters[i].Type,
name: method.Method.Parameters[i].Name));
}
}
else
{
arguments = null;
}
MethodFlags flags = MethodFlags.Default;
if (method.Method.IsStatic)
{
flags |= MethodFlags.Static;
}
return new MethodInfo(method.Method.Name, returnVal, flags, arguments,
defaultArguments: null);
}
private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, ITypeSymbol typeSymbol, string name)
{
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
var propUsage = PropertyUsageFlags.Default;
if (memberVariantType == VariantType.Nil)
propUsage |= PropertyUsageFlags.NilIsVariant;
string? className = null;
if (memberVariantType == VariantType.Object && typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
className = namedTypeSymbol.GetGodotScriptNativeClassName();
}
return new PropertyInfo(memberVariantType, name,
PropertyHint.None, string.Empty, propUsage, className, exported: false);
}
private static void GenerateHasMethodEntry(
string methodName,
StringBuilder source
)
{
source.Append(" ");
source.Append("if (method == MethodName.@");
source.Append(methodName);
source.Append(") {\n return true;\n }\n");
}
private static void GenerateMethodInvoker(
GodotMethodData method,
StringBuilder source
)
{
string methodName = method.Method.Name;
source.Append(" if (method == MethodName.@");
source.Append(methodName);
source.Append(" && args.Count == ");
source.Append(method.ParamTypes.Length);
source.Append(") {\n");
if (method.RetType != null)
source.Append(" var callRet = ");
else
source.Append(" ");
source.Append("@");
source.Append(methodName);
source.Append("(");
for (int i = 0; i < method.ParamTypes.Length; i++)
{
if (i != 0)
source.Append(", ");
source.AppendNativeVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"),
method.ParamTypeSymbols[i], method.ParamTypes[i]);
}
source.Append(");\n");
if (method.RetType != null)
{
source.Append(" ret = ");
source.AppendManagedToNativeVariantExpr("callRet",
method.RetType.Value.TypeSymbol, method.RetType.Value.MarshalType);
source.Append(";\n");
source.Append(" return true;\n");
}
else
{
source.Append(" ret = default;\n");
source.Append(" return true;\n");
}
source.Append(" }\n");
}
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptPathAttributeGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
if (context.IsGodotSourceGeneratorDisabled("ScriptPathAttribute"))
return;
if (context.IsGodotToolsProject())
return;
// NOTE: NotNullWhen diagnostics don't work on projects targeting .NET Standard 2.0
// ReSharper disable once ReplaceWithStringIsNullOrEmpty
if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDirBase64", out string? godotProjectDir) || godotProjectDir!.Length == 0)
{
if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out godotProjectDir) || godotProjectDir!.Length == 0)
{
throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
}
}
else
{
// Workaround for https://github.com/dotnet/roslyn/issues/51692
godotProjectDir = Encoding.UTF8.GetString(Convert.FromBase64String(godotProjectDir));
}
Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses = context
.Compilation.SyntaxTrees
.SelectMany(tree =>
tree.GetRoot().DescendantNodes()
.OfType<ClassDeclarationSyntax>()
// Ignore inner classes
.Where(cds => !cds.IsNested())
.SelectGodotScriptClasses(context.Compilation)
// Report and skip non-partial classes
.Where(x =>
{
if (x.cds.IsPartial())
return true;
return false;
})
)
.Where(x =>
// Ignore classes whose name is not the same as the file name
Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
.GroupBy<(ClassDeclarationSyntax cds, INamedTypeSymbol symbol), INamedTypeSymbol>(x => x.symbol, SymbolEqualityComparer.Default)
.ToDictionary<IGrouping<INamedTypeSymbol, (ClassDeclarationSyntax cds, INamedTypeSymbol symbol)>, INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>>(g => g.Key, g => g.Select(x => x.cds), SymbolEqualityComparer.Default);
var usedPaths = new HashSet<string>();
foreach (var godotClass in godotClasses)
{
VisitGodotScriptClass(context, godotProjectDir, usedPaths,
symbol: godotClass.Key,
classDeclarations: godotClass.Value);
}
if (godotClasses.Count <= 0)
return;
AddScriptTypesAssemblyAttr(context, godotClasses);
}
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
string godotProjectDir,
HashSet<string> usedPaths,
INamedTypeSymbol symbol,
IEnumerable<ClassDeclarationSyntax> classDeclarations
)
{
var attributes = new StringBuilder();
// Remember syntax trees for which we already added an attribute, to prevent unnecessary duplicates.
var attributedTrees = new List<SyntaxTree>();
foreach (var cds in classDeclarations)
{
if (attributedTrees.Contains(cds.SyntaxTree))
continue;
attributedTrees.Add(cds.SyntaxTree);
if (attributes.Length != 0)
attributes.Append("\n");
string scriptPath = RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir);
if (!usedPaths.Add(scriptPath))
{
context.ReportDiagnostic(Diagnostic.Create(
Common.MultipleClassesInGodotScriptRule,
cds.Identifier.GetLocation(),
symbol.Name
));
return;
}
attributes.Append(@"[ScriptPathAttribute(""res://");
attributes.Append(scriptPath);
attributes.Append(@""")]");
}
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedNameOmitGlobal() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
+ "_ScriptPath.generated";
var source = new StringBuilder();
// using Godot;
// namespace {classNs} {
// {attributesBuilder}
// partial class {className} { }
// }
source.Append("using Godot;\n");
if (hasNamespace)
{
source.Append("namespace ");
source.Append(classNs);
source.Append(" {\n\n");
}
source.Append(attributes);
source.Append("\npartial class ");
source.Append(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n}\n");
if (hasNamespace)
{
source.Append("\n}\n");
}
context.AddSource(uniqueHint, SourceText.From(source.ToString(), Encoding.UTF8));
}
private static void AddScriptTypesAssemblyAttr(GeneratorExecutionContext context,
Dictionary<INamedTypeSymbol, IEnumerable<ClassDeclarationSyntax>> godotClasses)
{
var sourceBuilder = new StringBuilder();
sourceBuilder.Append("[assembly:");
sourceBuilder.Append(GodotClasses.AssemblyHasScriptsAttr);
sourceBuilder.Append("(new System.Type[] {");
bool first = true;
foreach (var godotClass in godotClasses)
{
var qualifiedName = godotClass.Key.ToDisplayString(
NullableFlowState.NotNull, SymbolDisplayFormat.FullyQualifiedFormat
.WithGenericsOptions(SymbolDisplayGenericsOptions.None));
if (!first)
sourceBuilder.Append(", ");
first = false;
sourceBuilder.Append("typeof(");
sourceBuilder.Append(qualifiedName);
if (godotClass.Key.IsGenericType)
sourceBuilder.Append($"<{new string(',', godotClass.Key.TypeParameters.Count() - 1)}>");
sourceBuilder.Append(")");
}
sourceBuilder.Append("})]\n");
context.AddSource("AssemblyScriptTypes.generated",
SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
public void Initialize(GeneratorInitializationContext context)
{
}
private static string RelativeToDir(string path, string dir)
{
// Make sure the directory ends with a path separator
dir = Path.Combine(dir, " ").TrimEnd();
if (Path.DirectorySeparatorChar == '\\')
dir = dir.Replace("/", "\\") + "\\";
var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
// MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
}
}
}

View File

@@ -0,0 +1,947 @@
using System.Collections.Generic;
using System.Diagnostics;
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 ScriptPropertiesGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
if (context.IsGodotSourceGeneratorDisabled("ScriptProperties"))
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 != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedNameOmitGlobal() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null;
bool isToolClass = symbol.GetAttributes().Any(a => a.AttributeClass?.IsGodotToolAttribute() ?? false);
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
+ "_ScriptProperties.generated";
var source = new StringBuilder();
source.Append("using Godot;\n");
source.Append("using Godot.NativeInterop;\n");
source.Append("\n");
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.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
}
}
source.Append("partial class ");
source.Append(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
var members = symbol.GetMembers();
var propertySymbols = members
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
.Cast<IPropertySymbol>()
.Where(s => !s.IsIndexer && s.ExplicitInterfaceImplementations.Length == 0);
var fieldSymbols = members
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
.Cast<IFieldSymbol>();
var godotClassProperties = propertySymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
var godotClassFields = fieldSymbols.WhereIsGodotCompatibleType(typeCache).ToArray();
source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
source.Append(" /// <summary>\n")
.Append(" /// Cached StringNames for the properties and fields contained in this class, for fast lookup.\n")
.Append(" /// </summary>\n");
source.Append(
$" public new class PropertyName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.PropertyName {{\n");
// Generate cached StringNames for methods and properties, for fast lookup
foreach (var property in godotClassProperties)
{
string propertyName = property.PropertySymbol.Name;
source.Append(" /// <summary>\n")
.Append(" /// Cached name for the '")
.Append(propertyName)
.Append("' property.\n")
.Append(" /// </summary>\n");
source.Append(" public new static readonly global::Godot.StringName @");
source.Append(propertyName);
source.Append(" = \"");
source.Append(propertyName);
source.Append("\";\n");
}
foreach (var field in godotClassFields)
{
string fieldName = field.FieldSymbol.Name;
source.Append(" /// <summary>\n")
.Append(" /// Cached name for the '")
.Append(fieldName)
.Append("' field.\n")
.Append(" /// </summary>\n");
source.Append(" public new static readonly global::Godot.StringName @");
source.Append(fieldName);
source.Append(" = \"");
source.Append(fieldName);
source.Append("\";\n");
}
source.Append(" }\n"); // class GodotInternal
if (godotClassProperties.Length > 0 || godotClassFields.Length > 0)
{
// Generate SetGodotClassPropertyValue
bool allPropertiesAreReadOnly = godotClassFields.All(fi => fi.FieldSymbol.IsReadOnly) &&
godotClassProperties.All(pi => pi.PropertySymbol.IsReadOnly || pi.PropertySymbol.SetMethod!.IsInitOnly);
if (!allPropertiesAreReadOnly)
{
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(" protected override bool SetGodotClassPropertyValue(in godot_string_name name, ");
source.Append("in godot_variant value)\n {\n");
foreach (var property in godotClassProperties)
{
if (property.PropertySymbol.IsReadOnly || property.PropertySymbol.SetMethod!.IsInitOnly)
continue;
GeneratePropertySetter(property.PropertySymbol.Name,
property.PropertySymbol.Type, property.Type, source);
}
foreach (var field in godotClassFields)
{
if (field.FieldSymbol.IsReadOnly)
continue;
GeneratePropertySetter(field.FieldSymbol.Name,
field.FieldSymbol.Type, field.Type, source);
}
source.Append(" return base.SetGodotClassPropertyValue(name, value);\n");
source.Append(" }\n");
}
// Generate GetGodotClassPropertyValue
bool allPropertiesAreWriteOnly = godotClassFields.Length == 0 && godotClassProperties.All(pi => pi.PropertySymbol.IsWriteOnly);
if (!allPropertiesAreWriteOnly)
{
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(" protected override bool GetGodotClassPropertyValue(in godot_string_name name, ");
source.Append("out godot_variant value)\n {\n");
foreach (var property in godotClassProperties)
{
if (property.PropertySymbol.IsWriteOnly)
continue;
GeneratePropertyGetter(property.PropertySymbol.Name,
property.PropertySymbol.Type, property.Type, source);
}
foreach (var field in godotClassFields)
{
GeneratePropertyGetter(field.FieldSymbol.Name,
field.FieldSymbol.Type, field.Type, source);
}
source.Append(" return base.GetGodotClassPropertyValue(name, out value);\n");
source.Append(" }\n");
}
// Generate GetGodotPropertyList
const string DictionaryType = "global::System.Collections.Generic.List<global::Godot.Bridge.PropertyInfo>";
source.Append(" /// <summary>\n")
.Append(" /// Get the property information for all the properties declared in this class.\n")
.Append(" /// This method is used by Godot to register the available properties in the editor.\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 ")
.Append(DictionaryType)
.Append(" GetGodotPropertyList()\n {\n");
source.Append(" var properties = new ")
.Append(DictionaryType)
.Append("();\n");
// To retain the definition order (and display categories correctly), we want to
// iterate over fields and properties at the same time, sorted by line number.
var godotClassPropertiesAndFields = Enumerable.Empty<GodotPropertyOrFieldData>()
.Concat(godotClassProperties.Select(propertyData => new GodotPropertyOrFieldData(propertyData)))
.Concat(godotClassFields.Select(fieldData => new GodotPropertyOrFieldData(fieldData)))
.OrderBy(data => data.Symbol.Locations[0].Path())
.ThenBy(data => data.Symbol.Locations[0].StartLine());
foreach (var member in godotClassPropertiesAndFields)
{
foreach (var groupingInfo in DetermineGroupingPropertyInfo(member.Symbol))
AppendGroupingPropertyInfo(source, groupingInfo);
var propertyInfo = DeterminePropertyInfo(context, typeCache,
member.Symbol, member.Type);
if (propertyInfo == null)
continue;
if (propertyInfo.Value.Hint == PropertyHint.ToolButton && !isToolClass)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.OnlyToolClassesShouldUseExportToolButtonRule,
member.Symbol.Locations.FirstLocationWithSourceTreeOrDefault(),
member.Symbol.ToDisplayString()
));
continue;
}
AppendPropertyInfo(source, propertyInfo.Value);
}
source.Append(" return properties;\n");
source.Append(" }\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 void GeneratePropertySetter(
string propertyMemberName,
ITypeSymbol propertyTypeSymbol,
MarshalType propertyMarshalType,
StringBuilder source
)
{
source.Append(" ");
source.Append("if (name == PropertyName.@")
.Append(propertyMemberName)
.Append(") {\n")
.Append(" this.@")
.Append(propertyMemberName)
.Append(" = ")
.AppendNativeVariantToManagedExpr("value", propertyTypeSymbol, propertyMarshalType)
.Append(";\n")
.Append(" return true;\n")
.Append(" }\n");
}
private static void GeneratePropertyGetter(
string propertyMemberName,
ITypeSymbol propertyTypeSymbol,
MarshalType propertyMarshalType,
StringBuilder source
)
{
source.Append(" ");
source.Append("if (name == PropertyName.@")
.Append(propertyMemberName)
.Append(") {\n")
.Append(" value = ")
.AppendManagedToNativeVariantExpr("this.@" + propertyMemberName,
propertyTypeSymbol, propertyMarshalType)
.Append(";\n")
.Append(" return true;\n")
.Append(" }\n");
}
private static void AppendGroupingPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
{
source.Append(" properties.Add(new(type: (global::Godot.Variant.Type)")
.Append((int)VariantType.Nil)
.Append(", name: \"")
.Append(propertyInfo.Name)
.Append("\", hint: (global::Godot.PropertyHint)")
.Append((int)PropertyHint.None)
.Append(", hintString: \"")
.Append(propertyInfo.HintString)
.Append("\", usage: (global::Godot.PropertyUsageFlags)")
.Append((int)propertyInfo.Usage)
.Append(", exported: true));\n");
}
private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
{
source.Append(" properties.Add(new(type: (global::Godot.Variant.Type)")
.Append((int)propertyInfo.Type)
.Append(", name: PropertyName.@")
.Append(propertyInfo.Name)
.Append(", hint: (global::Godot.PropertyHint)")
.Append((int)propertyInfo.Hint)
.Append(", hintString: \"")
.Append(propertyInfo.HintString)
.Append("\", usage: (global::Godot.PropertyUsageFlags)")
.Append((int)propertyInfo.Usage)
.Append(", exported: ")
.Append(propertyInfo.Exported ? "true" : "false")
.Append("));\n");
}
private static IEnumerable<PropertyInfo> DetermineGroupingPropertyInfo(ISymbol memberSymbol)
{
foreach (var attr in memberSymbol.GetAttributes())
{
PropertyUsageFlags? propertyUsage = attr.AttributeClass?.FullQualifiedNameOmitGlobal() switch
{
GodotClasses.ExportCategoryAttr => PropertyUsageFlags.Category,
GodotClasses.ExportGroupAttr => PropertyUsageFlags.Group,
GodotClasses.ExportSubgroupAttr => PropertyUsageFlags.Subgroup,
_ => null
};
if (propertyUsage is null)
continue;
if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is string name)
{
string? hintString = null;
if (propertyUsage != PropertyUsageFlags.Category && attr.ConstructorArguments.Length > 1)
hintString = attr.ConstructorArguments[1].Value?.ToString();
yield return new PropertyInfo(VariantType.Nil, name, PropertyHint.None, hintString,
propertyUsage.Value, true);
}
}
}
private static PropertyInfo? DeterminePropertyInfo(
GeneratorExecutionContext context,
MarshalUtils.TypeCache typeCache,
ISymbol memberSymbol,
MarshalType marshalType
)
{
var exportAttr = memberSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.IsGodotExportAttribute() ?? false);
var exportToolButtonAttr = memberSymbol.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.IsGodotExportToolButtonAttribute() ?? false);
if (exportAttr != null && exportToolButtonAttr != null)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportToolButtonShouldNotBeUsedWithExportRule,
memberSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
memberSymbol.ToDisplayString()
));
return null;
}
var propertySymbol = memberSymbol as IPropertySymbol;
var fieldSymbol = memberSymbol as IFieldSymbol;
if (exportAttr != null && propertySymbol != null)
{
if (propertySymbol.GetMethod == null || propertySymbol.SetMethod == null || propertySymbol.SetMethod.IsInitOnly)
{
// Exports can be neither read-only nor write-only but the diagnostic errors for properties are already
// reported by ScriptPropertyDefValGenerator.cs so just quit early here.
return null;
}
}
if (exportToolButtonAttr != null && propertySymbol != null && propertySymbol.GetMethod == null)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportedPropertyIsWriteOnlyRule,
propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
propertySymbol.ToDisplayString()
));
return null;
}
if (exportToolButtonAttr != null && propertySymbol != null)
{
if (!PropertyIsExpressionBodiedAndReturnsNewCallable(context.Compilation, propertySymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportToolButtonMustBeExpressionBodiedProperty,
propertySymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
propertySymbol.ToDisplayString()
));
return null;
}
static bool PropertyIsExpressionBodiedAndReturnsNewCallable(Compilation compilation, IPropertySymbol? propertySymbol)
{
if (propertySymbol == null)
{
return false;
}
var propertyDeclarationSyntax = propertySymbol.DeclaringSyntaxReferences
.Select(r => r.GetSyntax() as PropertyDeclarationSyntax).FirstOrDefault();
if (propertyDeclarationSyntax == null || propertyDeclarationSyntax.Initializer != null)
{
return false;
}
if (propertyDeclarationSyntax.AccessorList != null)
{
var accessors = propertyDeclarationSyntax.AccessorList.Accessors;
foreach (var accessor in accessors)
{
if (!accessor.IsKind(SyntaxKind.GetAccessorDeclaration))
{
// Only getters are allowed.
return false;
}
if (!ExpressionBodyReturnsNewCallable(compilation, accessor.ExpressionBody))
{
return false;
}
}
}
else if (!ExpressionBodyReturnsNewCallable(compilation, propertyDeclarationSyntax.ExpressionBody))
{
return false;
}
return true;
}
static bool ExpressionBodyReturnsNewCallable(Compilation compilation, ArrowExpressionClauseSyntax? expressionSyntax)
{
if (expressionSyntax == null)
{
return false;
}
var semanticModel = compilation.GetSemanticModel(expressionSyntax.SyntaxTree);
switch (expressionSyntax.Expression)
{
case ImplicitObjectCreationExpressionSyntax creationExpression:
// We already validate that the property type must be 'Callable'
// so we can assume this constructor is valid.
return true;
case ObjectCreationExpressionSyntax creationExpression:
var typeSymbol = semanticModel.GetSymbolInfo(creationExpression.Type).Symbol as ITypeSymbol;
if (typeSymbol != null)
{
return typeSymbol.FullQualifiedNameOmitGlobal() == GodotClasses.Callable;
}
break;
case InvocationExpressionSyntax invocationExpression:
var methodSymbol = semanticModel.GetSymbolInfo(invocationExpression).Symbol as IMethodSymbol;
if (methodSymbol != null && methodSymbol.Name == "From")
{
return methodSymbol.ContainingType.FullQualifiedNameOmitGlobal() == GodotClasses.Callable;
}
break;
}
return false;
}
}
var memberType = propertySymbol?.Type ?? fieldSymbol!.Type;
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
string memberName = memberSymbol.Name;
string? hintString = null;
if (exportToolButtonAttr != null)
{
if (memberVariantType != VariantType.Callable)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.ExportToolButtonIsNotCallableRule,
memberSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
memberSymbol.ToDisplayString()
));
return null;
}
hintString = exportToolButtonAttr.ConstructorArguments[0].Value?.ToString() ?? "";
foreach (var namedArgument in exportToolButtonAttr.NamedArguments)
{
if (namedArgument is { Key: "Icon", Value.Value: string { Length: > 0 } })
{
hintString += $",{namedArgument.Value.Value}";
}
}
return new PropertyInfo(memberVariantType, memberName, PropertyHint.ToolButton,
hintString: hintString, PropertyUsageFlags.Editor, exported: true);
}
if (exportAttr == null)
{
return new PropertyInfo(memberVariantType, memberName, PropertyHint.None,
hintString: hintString, PropertyUsageFlags.ScriptVariable, exported: false);
}
if (!TryGetMemberExportHint(typeCache, memberType, exportAttr, memberVariantType,
isTypeArgument: false, out var hint, out hintString))
{
var constructorArguments = exportAttr.ConstructorArguments;
if (constructorArguments.Length > 0)
{
var hintValue = exportAttr.ConstructorArguments[0].Value;
hint = hintValue switch
{
null => PropertyHint.None,
int intValue => (PropertyHint)intValue,
_ => (PropertyHint)(long)hintValue
};
hintString = constructorArguments.Length > 1 ?
exportAttr.ConstructorArguments[1].Value?.ToString() :
null;
}
else
{
hint = PropertyHint.None;
}
}
var propUsage = PropertyUsageFlags.Default | PropertyUsageFlags.ScriptVariable;
if (memberVariantType == VariantType.Nil)
propUsage |= PropertyUsageFlags.NilIsVariant;
return new PropertyInfo(memberVariantType, memberName,
hint, hintString, propUsage, exported: true);
}
private static bool TryGetMemberExportHint(
MarshalUtils.TypeCache typeCache,
ITypeSymbol type, AttributeData exportAttr,
VariantType variantType, bool isTypeArgument,
out PropertyHint hint, out string? hintString
)
{
hint = PropertyHint.None;
hintString = null;
if (variantType == VariantType.Nil)
return true; // Variant, no export hint
if (variantType == VariantType.Int &&
type.IsValueType && type.TypeKind == TypeKind.Enum)
{
bool hasFlagsAttr = type.GetAttributes()
.Any(a => a.AttributeClass?.IsSystemFlagsAttribute() ?? false);
hint = hasFlagsAttr ? PropertyHint.Flags : PropertyHint.Enum;
var members = type.GetMembers();
var enumFields = members
.Where(s => s.Kind == SymbolKind.Field && s.IsStatic &&
s.DeclaredAccessibility == Accessibility.Public &&
!s.IsImplicitlyDeclared)
.Cast<IFieldSymbol>().ToArray();
var hintStringBuilder = new StringBuilder();
var nameOnlyHintStringBuilder = new StringBuilder();
// True: enum Foo { Bar, Baz, Qux }
// True: enum Foo { Bar = 0, Baz = 1, Qux = 2 }
// False: enum Foo { Bar = 0, Baz = 7, Qux = 5 }
bool usesDefaultValues = true;
for (int i = 0; i < enumFields.Length; i++)
{
var enumField = enumFields[i];
if (i > 0)
{
hintStringBuilder.Append(",");
nameOnlyHintStringBuilder.Append(",");
}
string enumFieldName = enumField.Name;
hintStringBuilder.Append(enumFieldName);
nameOnlyHintStringBuilder.Append(enumFieldName);
long val = enumField.ConstantValue switch
{
sbyte v => v,
short v => v,
int v => v,
long v => v,
byte v => v,
ushort v => v,
uint v => v,
ulong v => (long)v,
_ => 0
};
uint expectedVal = (uint)(hint == PropertyHint.Flags ? 1 << i : i);
if (val != expectedVal)
usesDefaultValues = false;
hintStringBuilder.Append(":");
hintStringBuilder.Append(val);
}
hintString = !usesDefaultValues ?
hintStringBuilder.ToString() :
// If we use the format NAME:VAL, that's what the editor displays.
// That's annoying if the user is not using custom values for the enum constants.
// This may not be needed in the future if the editor is changed to not display values.
nameOnlyHintStringBuilder.ToString();
return true;
}
if (variantType == VariantType.Object && type is INamedTypeSymbol memberNamedType)
{
if (TryGetNodeOrResourceType(exportAttr, out hint, out hintString))
{
return true;
}
if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Resource"))
{
hint = PropertyHint.ResourceType;
hintString = GetTypeName(memberNamedType);
return true;
}
if (memberNamedType.InheritsFrom("GodotSharp", "Godot.Node"))
{
hint = PropertyHint.NodeType;
hintString = GetTypeName(memberNamedType);
return true;
}
}
static bool TryGetNodeOrResourceType(AttributeData exportAttr, out PropertyHint hint, out string? hintString)
{
hint = PropertyHint.None;
hintString = null;
if (exportAttr.ConstructorArguments.Length <= 1) return false;
var hintValue = exportAttr.ConstructorArguments[0].Value;
var hintEnum = hintValue switch
{
null => PropertyHint.None,
int intValue => (PropertyHint)intValue,
_ => (PropertyHint)(long)hintValue
};
if (!hintEnum.HasFlag(PropertyHint.NodeType) && !hintEnum.HasFlag(PropertyHint.ResourceType))
return false;
var hintStringValue = exportAttr.ConstructorArguments[1].Value?.ToString();
if (string.IsNullOrWhiteSpace(hintStringValue))
{
return false;
}
hint = hintEnum;
hintString = hintStringValue;
return true;
}
static string GetTypeName(INamedTypeSymbol memberSymbol)
{
if (memberSymbol.GetAttributes()
.Any(a => a.AttributeClass?.IsGodotGlobalClassAttribute() ?? false))
{
return memberSymbol.Name;
}
return memberSymbol.GetGodotScriptNativeClassName()!;
}
static bool GetStringArrayEnumHint(VariantType elementVariantType,
AttributeData exportAttr, out string? hintString)
{
var constructorArguments = exportAttr.ConstructorArguments;
if (constructorArguments.Length > 0)
{
var presetHintValue = exportAttr.ConstructorArguments[0].Value;
PropertyHint presetHint = presetHintValue switch
{
null => PropertyHint.None,
int intValue => (PropertyHint)intValue,
_ => (PropertyHint)(long)presetHintValue
};
if (presetHint == PropertyHint.Enum)
{
string? presetHintString = constructorArguments.Length > 1 ?
exportAttr.ConstructorArguments[1].Value?.ToString() :
null;
hintString = (int)elementVariantType + "/" + (int)PropertyHint.Enum + ":";
if (presetHintString != null)
hintString += presetHintString;
return true;
}
}
hintString = null;
return false;
}
if (!isTypeArgument && variantType == VariantType.Array)
{
var elementType = MarshalUtils.GetArrayElementType(type);
if (elementType == null)
return false; // Non-generic Array, so there's no hint to add.
if (elementType.TypeKind == TypeKind.TypeParameter)
return false; // The generic is not constructed, we can't really hint anything.
var elementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementType, typeCache)!.Value;
var elementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(elementMarshalType)!.Value;
bool isPresetHint = false;
if (elementVariantType == VariantType.String || elementVariantType == VariantType.StringName)
isPresetHint = GetStringArrayEnumHint(elementVariantType, exportAttr, out hintString);
if (!isPresetHint)
{
bool hintRes = TryGetMemberExportHint(typeCache, elementType,
exportAttr, elementVariantType, isTypeArgument: true,
out var elementHint, out var elementHintString);
// Format: type/hint:hint_string
if (hintRes)
{
hintString = (int)elementVariantType + "/" + (int)elementHint + ":";
if (elementHintString != null)
hintString += elementHintString;
}
else
{
hintString = (int)elementVariantType + "/" + (int)PropertyHint.None + ":";
}
}
hint = PropertyHint.TypeString;
return hintString != null;
}
if (!isTypeArgument && variantType == VariantType.PackedStringArray)
{
if (GetStringArrayEnumHint(VariantType.String, exportAttr, out hintString))
{
hint = PropertyHint.TypeString;
return true;
}
}
if (!isTypeArgument && variantType == VariantType.Dictionary)
{
var elementTypes = MarshalUtils.GetGenericElementTypes(type);
if (elementTypes == null)
return false; // Non-generic Dictionary, so there's no hint to add
Debug.Assert(elementTypes.Length == 2);
var keyElementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementTypes[0], typeCache);
var valueElementMarshalType = MarshalUtils.ConvertManagedTypeToMarshalType(elementTypes[1], typeCache);
if (keyElementMarshalType == null || valueElementMarshalType == null)
{
// To maintain compatibility with previous versions of Godot before 4.4,
// we must preserve the old behavior for generic dictionaries with non-marshallable
// generic type arguments.
return false;
}
var keyElementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(keyElementMarshalType.Value)!.Value;
var keyIsPresetHint = false;
var keyHintString = (string?)null;
if (keyElementVariantType == VariantType.String || keyElementVariantType == VariantType.StringName)
keyIsPresetHint = GetStringArrayEnumHint(keyElementVariantType, exportAttr, out keyHintString);
if (!keyIsPresetHint)
{
bool hintRes = TryGetMemberExportHint(typeCache, elementTypes[0],
exportAttr, keyElementVariantType, isTypeArgument: true,
out var keyElementHint, out var keyElementHintString);
// Format: type/hint:hint_string
if (hintRes)
{
keyHintString = (int)keyElementVariantType + "/" + (int)keyElementHint + ":";
if (keyElementHintString != null)
keyHintString += keyElementHintString;
}
else
{
keyHintString = (int)keyElementVariantType + "/" + (int)PropertyHint.None + ":";
}
}
var valueElementVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(valueElementMarshalType.Value)!.Value;
var valueIsPresetHint = false;
var valueHintString = (string?)null;
if (valueElementVariantType == VariantType.String || valueElementVariantType == VariantType.StringName)
valueIsPresetHint = GetStringArrayEnumHint(valueElementVariantType, exportAttr, out valueHintString);
if (!valueIsPresetHint)
{
bool hintRes = TryGetMemberExportHint(typeCache, elementTypes[1],
exportAttr, valueElementVariantType, isTypeArgument: true,
out var valueElementHint, out var valueElementHintString);
// Format: type/hint:hint_string
if (hintRes)
{
valueHintString = (int)valueElementVariantType + "/" + (int)valueElementHint + ":";
if (valueElementHintString != null)
valueHintString += valueElementHintString;
}
else
{
valueHintString = (int)valueElementVariantType + "/" + (int)PropertyHint.None + ":";
}
}
hint = PropertyHint.TypeString;
hintString = keyHintString != null && valueHintString != null ? $"{keyHintString};{valueHintString}" : null;
return hintString != null;
}
return false;
}
}
}

View File

@@ -0,0 +1,569 @@
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.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
}
}
source.Append("partial class ");
source.Append(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
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);
var initializerValue = propertyDeclarationSyntax.Initializer.Value;
if (!IsStaticallyResolvable(initializerValue, sm))
value = "default";
else
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 IsStaticallyResolvable(ExpressionSyntax expression, SemanticModel semanticModel)
{
// Handle literals (e.g., `10`, `"string"`, `true`, etc.)
if (expression is LiteralExpressionSyntax)
{
return true;
}
// Handle negative literals (e.g., `-10`)
if (expression is PrefixUnaryExpressionSyntax { Operand: LiteralExpressionSyntax } &&
expression.Kind() == SyntaxKind.UnaryMinusExpression)
{
return true;
}
// Handle identifiers (e.g., variable names)
if (expression is IdentifierNameSyntax identifier)
{
var symbolInfo = semanticModel.GetSymbolInfo(identifier).Symbol;
// Ensure it's a static member
return symbolInfo is { IsStatic: true };
}
// Handle member access (e.g., `MyClass.StaticValue`)
if (expression is MemberAccessExpressionSyntax memberAccess)
{
var symbolInfo = semanticModel.GetSymbolInfo(memberAccess).Symbol;
// Ensure it's referring to a static member
return symbolInfo is { IsStatic: true };
}
// Handle object creation expressions (e.g., `new Vector2(1.0f, 2.0f)`)
if (expression is ObjectCreationExpressionSyntax objectCreation)
{
// Recursively ensure all its arguments are self-contained
if (objectCreation.ArgumentList == null)
{
return true;
}
foreach (var argument in objectCreation.ArgumentList.Arguments)
{
if (!IsStaticallyResolvable(argument.Expression, semanticModel))
{
return false;
}
}
return true;
}
if (expression is ImplicitObjectCreationExpressionSyntax)
{
return true;
}
if (expression is InvocationExpressionSyntax invocationExpression)
{
// Resolve the method being invoked
var symbolInfo = semanticModel.GetSymbolInfo(invocationExpression).Symbol;
if (symbolInfo is IMethodSymbol methodSymbol)
{
// Ensure the method is static
if (methodSymbol.IsStatic)
{
return true;
}
}
}
if (expression is InterpolatedStringExpressionSyntax interpolatedString)
{
foreach (var content in interpolatedString.Contents)
{
if (content is not InterpolationSyntax interpolation)
{
continue;
}
// Analyze the expression inside `${...}`
var interpolatedExpression = interpolation.Expression;
if (!IsStaticallyResolvable(interpolatedExpression, semanticModel))
{
return false;
}
}
return true;
}
if (expression is InitializerExpressionSyntax initializerExpressionSyntax)
{
foreach (var content in initializerExpressionSyntax.Expressions)
{
if (!IsStaticallyResolvable(content, semanticModel))
{
return false;
}
}
return true;
}
// Handle other expressions conservatively (e.g., method calls, instance references, etc.)
return false;
}
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; }
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.CodeAnalysis;
namespace Godot.SourceGenerators
{
// Placeholder. Once we switch to native extensions this will act as the registrar for all
// user Godot classes in the assembly. Think of it as something similar to `register_types`.
public class ScriptRegistrarGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
throw new System.NotImplementedException();
}
public void Execute(GeneratorExecutionContext context)
{
throw new System.NotImplementedException();
}
}
}

View File

@@ -0,0 +1,298 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptSerializationGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
if (context.IsGodotSourceGeneratorDisabled("ScriptSerialization"))
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 != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedNameOmitGlobal() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null;
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
+ "_ScriptSerialization.generated";
var source = new StringBuilder();
source.Append("using Godot;\n");
source.Append("using Godot.NativeInterop;\n");
source.Append("\n");
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.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
}
}
source.Append("partial class ");
source.Append(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
var members = symbol.GetMembers();
var propertySymbols = members
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Property)
.Cast<IPropertySymbol>()
.Where(s => !s.IsIndexer && s.ExplicitInterfaceImplementations.Length == 0);
var fieldSymbols = members
.Where(s => !s.IsStatic && s.Kind == SymbolKind.Field && !s.IsImplicitlyDeclared)
.Cast<IFieldSymbol>();
// 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.
var godotClassProperties = propertySymbols.Where(property => !(property.IsReadOnly || property.IsWriteOnly || property.SetMethod!.IsInitOnly))
.WhereIsGodotCompatibleType(typeCache)
.ToArray();
var godotClassFields = fieldSymbols.Where(property => !property.IsReadOnly)
.WhereIsGodotCompatibleType(typeCache)
.ToArray();
var signalDelegateSymbols = members
.Where(s => s.Kind == SymbolKind.NamedType)
.Cast<INamedTypeSymbol>()
.Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate)
.Where(s => s.GetAttributes()
.Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false));
List<GodotSignalDelegateData> godotSignalDelegates = new();
foreach (var signalDelegateSymbol in signalDelegateSymbols)
{
if (!signalDelegateSymbol.Name.EndsWith(ScriptSignalsGenerator.SignalDelegateSuffix))
continue;
string signalName = signalDelegateSymbol.Name;
signalName = signalName.Substring(0,
signalName.Length - ScriptSignalsGenerator.SignalDelegateSuffix.Length);
var invokeMethodData = signalDelegateSymbol
.DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache);
if (invokeMethodData == null)
continue;
godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value));
}
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(
" protected override void SaveGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n {\n");
source.Append(" base.SaveGodotObjectData(info);\n");
// Save properties
foreach (var property in godotClassProperties)
{
string propertyName = property.PropertySymbol.Name;
source.Append(" info.AddProperty(PropertyName.@")
.Append(propertyName)
.Append(", ")
.AppendManagedToVariantExpr(string.Concat("this.@", propertyName),
property.PropertySymbol.Type, property.Type)
.Append(");\n");
}
// Save fields
foreach (var field in godotClassFields)
{
string fieldName = field.FieldSymbol.Name;
source.Append(" info.AddProperty(PropertyName.@")
.Append(fieldName)
.Append(", ")
.AppendManagedToVariantExpr(string.Concat("this.@", fieldName),
field.FieldSymbol.Type, field.Type)
.Append(");\n");
}
// Save signal events
foreach (var signalDelegate in godotSignalDelegates)
{
string signalName = signalDelegate.Name;
source.Append(" info.AddSignalEventDelegate(SignalName.@")
.Append(signalName)
.Append(", this.backing_")
.Append(signalName)
.Append(");\n");
}
source.Append(" }\n");
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(
" protected override void RestoreGodotObjectData(global::Godot.Bridge.GodotSerializationInfo info)\n {\n");
source.Append(" base.RestoreGodotObjectData(info);\n");
// Restore properties
foreach (var property in godotClassProperties)
{
string propertyName = property.PropertySymbol.Name;
source.Append(" if (info.TryGetProperty(PropertyName.@")
.Append(propertyName)
.Append(", out var _value_")
.Append(propertyName)
.Append("))\n")
.Append(" this.@")
.Append(propertyName)
.Append(" = ")
.AppendVariantToManagedExpr(string.Concat("_value_", propertyName),
property.PropertySymbol.Type, property.Type)
.Append(";\n");
}
// Restore fields
foreach (var field in godotClassFields)
{
string fieldName = field.FieldSymbol.Name;
source.Append(" if (info.TryGetProperty(PropertyName.@")
.Append(fieldName)
.Append(", out var _value_")
.Append(fieldName)
.Append("))\n")
.Append(" this.@")
.Append(fieldName)
.Append(" = ")
.AppendVariantToManagedExpr(string.Concat("_value_", fieldName),
field.FieldSymbol.Type, field.Type)
.Append(";\n");
}
// Restore signal events
foreach (var signalDelegate in godotSignalDelegates)
{
string signalName = signalDelegate.Name;
string signalDelegateQualifiedName = signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal();
source.Append(" if (info.TryGetSignalEventDelegate<")
.Append(signalDelegateQualifiedName)
.Append(">(SignalName.@")
.Append(signalName)
.Append(", out var _value_")
.Append(signalName)
.Append("))\n")
.Append(" this.backing_")
.Append(signalName)
.Append(" = _value_")
.Append(signalName)
.Append(";\n");
}
source.Append(" }\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));
}
}
}

View File

@@ -0,0 +1,546 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Godot.SourceGenerators
{
[Generator]
public class ScriptSignalsGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
}
public void Execute(GeneratorExecutionContext context)
{
if (context.IsGodotSourceGeneratorDisabled("ScriptSignals"))
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);
}
}
}
internal static string SignalDelegateSuffix = "EventHandler";
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
MarshalUtils.TypeCache typeCache,
INamedTypeSymbol symbol
)
{
INamespaceSymbol namespaceSymbol = symbol.ContainingNamespace;
string classNs = namespaceSymbol != null && !namespaceSymbol.IsGlobalNamespace ?
namespaceSymbol.FullQualifiedNameOmitGlobal() :
string.Empty;
bool hasNamespace = classNs.Length != 0;
bool isInnerClass = symbol.ContainingType != null;
string uniqueHint = symbol.FullQualifiedNameOmitGlobal().SanitizeQualifiedNameForUniqueHint()
+ "_ScriptSignals.generated";
var source = new StringBuilder();
source.Append("using Godot;\n");
source.Append("using Godot.NativeInterop;\n");
source.Append("\n");
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.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
}
}
source.Append("partial class ");
source.Append(symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
source.Append("\n{\n");
var members = symbol.GetMembers();
var signalDelegateSymbols = members
.Where(s => s.Kind == SymbolKind.NamedType)
.Cast<INamedTypeSymbol>()
.Where(namedTypeSymbol => namedTypeSymbol.TypeKind == TypeKind.Delegate)
.Where(s => s.GetAttributes()
.Any(a => a.AttributeClass?.IsGodotSignalAttribute() ?? false));
List<GodotSignalDelegateData> godotSignalDelegates = new();
foreach (var signalDelegateSymbol in signalDelegateSymbols)
{
if (!signalDelegateSymbol.Name.EndsWith(SignalDelegateSuffix))
{
context.ReportDiagnostic(Diagnostic.Create(
Common.SignalDelegateMissingSuffixRule,
signalDelegateSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
signalDelegateSymbol.ToDisplayString()
));
continue;
}
string signalName = signalDelegateSymbol.Name;
signalName = signalName.Substring(0, signalName.Length - SignalDelegateSuffix.Length);
var invokeMethodData = signalDelegateSymbol
.DelegateInvokeMethod?.HasGodotCompatibleSignature(typeCache);
if (invokeMethodData == null)
{
if (signalDelegateSymbol.DelegateInvokeMethod is IMethodSymbol methodSymbol)
{
foreach (var parameter in methodSymbol.Parameters)
{
if (parameter.RefKind != RefKind.None)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.SignalParameterTypeNotSupportedRule,
parameter.Locations.FirstLocationWithSourceTreeOrDefault(),
parameter.ToDisplayString()
));
continue;
}
var marshalType = MarshalUtils.ConvertManagedTypeToMarshalType(parameter.Type, typeCache);
if (marshalType == null)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.SignalParameterTypeNotSupportedRule,
parameter.Locations.FirstLocationWithSourceTreeOrDefault(),
parameter.ToDisplayString()
));
}
}
if (!methodSymbol.ReturnsVoid)
{
context.ReportDiagnostic(Diagnostic.Create(
Common.SignalDelegateSignatureMustReturnVoidRule,
signalDelegateSymbol.Locations.FirstLocationWithSourceTreeOrDefault(),
signalDelegateSymbol.ToDisplayString()
));
}
}
continue;
}
godotSignalDelegates.Add(new(signalName, signalDelegateSymbol, invokeMethodData.Value));
}
source.Append("#pragma warning disable CS0109 // Disable warning about redundant 'new' keyword\n");
source.Append(" /// <summary>\n")
.Append(" /// Cached StringNames for the signals contained in this class, for fast lookup.\n")
.Append(" /// </summary>\n");
source.Append(
$" public new class SignalName : {symbol.BaseType!.FullQualifiedNameIncludeGlobal()}.SignalName {{\n");
// Generate cached StringNames for methods and properties, for fast lookup
foreach (var signalDelegate in godotSignalDelegates)
{
string signalName = signalDelegate.Name;
source.Append(" /// <summary>\n")
.Append(" /// Cached name for the '")
.Append(signalName)
.Append("' signal.\n")
.Append(" /// </summary>\n");
source.Append(" public new static readonly global::Godot.StringName @");
source.Append(signalName);
source.Append(" = \"");
source.Append(signalName);
source.Append("\";\n");
}
source.Append(" }\n"); // class GodotInternal
// Generate GetGodotSignalList
if (godotSignalDelegates.Count > 0)
{
const string ListType = "global::System.Collections.Generic.List<global::Godot.Bridge.MethodInfo>";
source.Append(" /// <summary>\n")
.Append(" /// Get the signal information for all the signals declared in this class.\n")
.Append(" /// This method is used by Godot to register the available signals in the editor.\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 ")
.Append(ListType)
.Append(" GetGodotSignalList()\n {\n");
source.Append(" var signals = new ")
.Append(ListType)
.Append("(")
.Append(godotSignalDelegates.Count)
.Append(");\n");
foreach (var signalDelegateData in godotSignalDelegates)
{
var methodInfo = DetermineMethodInfo(signalDelegateData);
AppendMethodInfo(source, methodInfo);
}
source.Append(" return signals;\n");
source.Append(" }\n");
}
source.Append("#pragma warning restore CS0109\n");
// Generate signal event
foreach (var signalDelegate in godotSignalDelegates)
{
string signalName = signalDelegate.Name;
// TODO: Hide backing event from code-completion and debugger
// The reason we have a backing field is to hide the invoke method from the event,
// as it doesn't emit the signal, only the event delegates. This can confuse users.
// Maybe we should directly connect the delegates, as we do with native signals?
source.Append(" private ")
.Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal())
.Append(" backing_")
.Append(signalName)
.Append(";\n");
source.Append(
$" /// <inheritdoc cref=\"{signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal()}\"/>\n");
source.Append($" {signalDelegate.DelegateSymbol.GetAccessibilityKeyword()} event ")
.Append(signalDelegate.DelegateSymbol.FullQualifiedNameIncludeGlobal())
.Append(" @")
.Append(signalName)
.Append(" {\n")
.Append(" add => backing_")
.Append(signalName)
.Append(" += value;\n")
.Append(" remove => backing_")
.Append(signalName)
.Append(" -= value;\n")
.Append("}\n");
// Generate EmitSignal{EventName} method to raise the event
var invokeMethodSymbol = signalDelegate.InvokeMethodData.Method;
int paramCount = invokeMethodSymbol.Parameters.Length;
string raiseMethodModifiers = signalDelegate.DelegateSymbol.ContainingType.IsSealed ?
"private" :
"protected";
source.Append($" {raiseMethodModifiers} void EmitSignal{signalName}(");
for (int i = 0; i < paramCount; i++)
{
var paramSymbol = invokeMethodSymbol.Parameters[i];
source.Append($"{paramSymbol.Type.FullQualifiedNameIncludeGlobal()} @{paramSymbol.Name}");
if (i < paramCount - 1)
{
source.Append(", ");
}
}
source.Append(")\n");
source.Append(" {\n");
source.Append($" EmitSignal(SignalName.{signalName}");
foreach (var paramSymbol in invokeMethodSymbol.Parameters)
{
// Enums must be converted to the underlying type before they can be implicitly converted to Variant
if (paramSymbol.Type.TypeKind == TypeKind.Enum)
{
var underlyingType = ((INamedTypeSymbol)paramSymbol.Type).EnumUnderlyingType!;
source.Append($", ({underlyingType.FullQualifiedNameIncludeGlobal()})@{paramSymbol.Name}");
continue;
}
source.Append($", @{paramSymbol.Name}");
}
source.Append(");\n");
source.Append(" }\n");
}
// Generate RaiseGodotClassSignalCallbacks
if (godotSignalDelegates.Count > 0)
{
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(
" protected override void RaiseGodotClassSignalCallbacks(in godot_string_name signal, ");
source.Append("NativeVariantPtrArgs args)\n {\n");
foreach (var signal in godotSignalDelegates)
{
GenerateSignalEventInvoker(signal, source);
}
source.Append(" base.RaiseGodotClassSignalCallbacks(signal, args);\n");
source.Append(" }\n");
}
// Generate HasGodotClassSignal
if (godotSignalDelegates.Count > 0)
{
source.Append(" /// <inheritdoc/>\n");
source.Append(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]\n");
source.Append(
" protected override bool HasGodotClassSignal(in godot_string_name signal)\n {\n");
foreach (var signal in godotSignalDelegates)
{
GenerateHasSignalEntry(signal.Name, source);
}
source.Append(" return base.HasGodotClassSignal(signal);\n");
source.Append(" }\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 void AppendMethodInfo(StringBuilder source, MethodInfo methodInfo)
{
source.Append(" signals.Add(new(name: SignalName.@")
.Append(methodInfo.Name)
.Append(", returnVal: ");
AppendPropertyInfo(source, methodInfo.ReturnVal);
source.Append(", flags: (global::Godot.MethodFlags)")
.Append((int)methodInfo.Flags)
.Append(", arguments: ");
if (methodInfo.Arguments is { Count: > 0 })
{
source.Append("new() { ");
foreach (var param in methodInfo.Arguments)
{
AppendPropertyInfo(source, param);
// C# allows colon after the last element
source.Append(", ");
}
source.Append(" }");
}
else
{
source.Append("null");
}
source.Append(", defaultArguments: null));\n");
}
private static void AppendPropertyInfo(StringBuilder source, PropertyInfo propertyInfo)
{
source.Append("new(type: (global::Godot.Variant.Type)")
.Append((int)propertyInfo.Type)
.Append(", name: \"")
.Append(propertyInfo.Name)
.Append("\", hint: (global::Godot.PropertyHint)")
.Append((int)propertyInfo.Hint)
.Append(", hintString: \"")
.Append(propertyInfo.HintString)
.Append("\", usage: (global::Godot.PropertyUsageFlags)")
.Append((int)propertyInfo.Usage)
.Append(", exported: ")
.Append(propertyInfo.Exported ? "true" : "false");
if (propertyInfo.ClassName != null)
{
source.Append(", className: new global::Godot.StringName(\"")
.Append(propertyInfo.ClassName)
.Append("\")");
}
source.Append(")");
}
private static MethodInfo DetermineMethodInfo(GodotSignalDelegateData signalDelegateData)
{
var invokeMethodData = signalDelegateData.InvokeMethodData;
PropertyInfo returnVal;
if (invokeMethodData.RetType != null)
{
returnVal = DeterminePropertyInfo(invokeMethodData.RetType.Value.MarshalType,
invokeMethodData.RetType.Value.TypeSymbol,
name: string.Empty);
}
else
{
returnVal = new PropertyInfo(VariantType.Nil, string.Empty, PropertyHint.None,
hintString: null, PropertyUsageFlags.Default, exported: false);
}
int paramCount = invokeMethodData.ParamTypes.Length;
List<PropertyInfo>? arguments;
if (paramCount > 0)
{
arguments = new(capacity: paramCount);
for (int i = 0; i < paramCount; i++)
{
arguments.Add(DeterminePropertyInfo(invokeMethodData.ParamTypes[i],
invokeMethodData.Method.Parameters[i].Type,
name: invokeMethodData.Method.Parameters[i].Name));
}
}
else
{
arguments = null;
}
return new MethodInfo(signalDelegateData.Name, returnVal, MethodFlags.Default, arguments,
defaultArguments: null);
}
private static PropertyInfo DeterminePropertyInfo(MarshalType marshalType, ITypeSymbol typeSymbol, string name)
{
var memberVariantType = MarshalUtils.ConvertMarshalTypeToVariantType(marshalType)!.Value;
var propUsage = PropertyUsageFlags.Default;
if (memberVariantType == VariantType.Nil)
propUsage |= PropertyUsageFlags.NilIsVariant;
string? className = null;
if (memberVariantType == VariantType.Object && typeSymbol is INamedTypeSymbol namedTypeSymbol)
{
className = namedTypeSymbol.GetGodotScriptNativeClassName();
}
return new PropertyInfo(memberVariantType, name,
PropertyHint.None, string.Empty, propUsage, className, exported: false);
}
private static void GenerateHasSignalEntry(
string signalName,
StringBuilder source
)
{
source.Append(" ");
source.Append("if (signal == SignalName.@");
source.Append(signalName);
source.Append(") {\n return true;\n }\n");
}
private static void GenerateSignalEventInvoker(
GodotSignalDelegateData signal,
StringBuilder source
)
{
string signalName = signal.Name;
var invokeMethodData = signal.InvokeMethodData;
source.Append(" if (signal == SignalName.@");
source.Append(signalName);
source.Append(" && args.Count == ");
source.Append(invokeMethodData.ParamTypes.Length);
source.Append(") {\n");
source.Append(" backing_");
source.Append(signalName);
source.Append("?.Invoke(");
for (int i = 0; i < invokeMethodData.ParamTypes.Length; i++)
{
if (i != 0)
source.Append(", ");
source.AppendNativeVariantToManagedExpr(string.Concat("args[", i.ToString(), "]"),
invokeMethodData.ParamTypeSymbols[i], invokeMethodData.ParamTypes[i]);
}
source.Append(");\n");
source.Append(" return;\n");
source.Append(" }\n");
}
}
}