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
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:
@@ -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)
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue)]
|
||||
public sealed class NotNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
@@ -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"));
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -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>
|
@@ -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";
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
}
|
||||
}
|
@@ -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, ")")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user