Add C# source generator for a new ScriptPath attribute

This source generator adds a newly introduced attribute,
`ScriptPath` to all classes that:

- Are top-level classes (not inner/nested).
- Have the `partial` modifier.
- Inherit `Godot.Object`.
- The class name matches the file name.

A build error is thrown if the generator finds a class that meets these
conditions but is not declared `partial`, unless the class is annotated
with the `DisableGodotGenerators` attribute.

We also generate an `AssemblyHasScripts` assembly attribute which Godot
uses to get all the script classes in the assembly, eliminating the need
for Godot to search them. We can also avoid searching in assemblies that
don't have this attribute. This will be good for performance in the
future once we support multiple assemblies with Godot script classes.

This is an example of what the generated code looks like:

```
using Godot;
namespace Foo {
	[ScriptPathAttribute("res://Player.cs")]
	// Multiple partial declarations are allowed
	[ScriptPathAttribute("res://Foo/Player.cs")]
	partial class Player {}
}

[assembly:AssemblyHasScripts(new System.Type[] { typeof(Foo.Player) })]
```

The new attributes replace script metadata which we were generating by
determining the namespace of script classes with a very simple parser.
This fixes several issues with the old approach related to parser
errors and conditional compilation.
It also makes the task part of the MSBuild project build, rather than
a separate step executed by the Godot editor.
This commit is contained in:
Ignacio Etcheverry 2021-03-06 00:12:42 +01:00
parent d4191e48c5
commit e2afe700f6
41 changed files with 661 additions and 1359 deletions

View file

@ -0,0 +1,3 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\SdkPackageVersions.props" />
</Project>

View file

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<PackageVersion_Godot_NET_Sdk>4.0.0-dev4</PackageVersion_Godot_NET_Sdk>
<PackageVersion_Godot_SourceGenerators>4.0.0-dev1</PackageVersion_Godot_SourceGenerators>
</PropertyGroup>
</Project>

View file

@ -21,6 +21,18 @@ def build_godot_net_sdk(source, target, env):
# No need to copy targets. The Godot.NET.Sdk csproj takes care of copying them.
def get_nupkgs_versions(props_file):
import xml.etree.ElementTree as ET
tree = ET.parse(props_file)
root = tree.getroot()
return {
"Godot.NET.Sdk": root.find("./PropertyGroup/PackageVersion_Godot_NET_Sdk").text.strip(),
"Godot.SourceGenerators": root.find("./PropertyGroup/PackageVersion_Godot_SourceGenerators").text.strip(),
}
def build(env_mono):
assert env_mono["tools"]
@ -30,14 +42,12 @@ def build(env_mono):
module_dir = os.getcwd()
package_version_file = os.path.join(
module_dir, "editor", "Godot.NET.Sdk", "Godot.NET.Sdk", "Godot.NET.Sdk_PackageVersion.txt"
)
nupkgs_versions = get_nupkgs_versions(os.path.join(module_dir, "SdkPackageVersions.props"))
with open(package_version_file, mode="r") as f:
version = f.read().strip()
target_filenames = ["Godot.NET.Sdk.%s.nupkg" % version]
target_filenames = [
"Godot.NET.Sdk.%s.nupkg" % nupkgs_versions["Godot.NET.Sdk"],
"Godot.SourceGenerators.%s.nupkg" % nupkgs_versions["Godot.SourceGenerators"],
]
targets = [os.path.join(nupkgs_dir, filename) for filename in target_filenames]

View file

@ -31,6 +31,7 @@
#include "csharp_script.h"
#include <mono/metadata/threads.h>
#include <mono/metadata/tokentype.h>
#include <stdint.h>
#include "core/config/project_settings.h"
@ -1182,46 +1183,56 @@ void CSharpLanguage::reload_assemblies(bool p_soft_reload) {
}
#endif
void CSharpLanguage::_load_scripts_metadata() {
scripts_metadata.clear();
void CSharpLanguage::lookup_script_for_class(GDMonoClass *p_class) {
if (!p_class->has_attribute(CACHED_CLASS(ScriptPathAttribute))) {
return;
}
String scripts_metadata_filename = "scripts_metadata.";
MonoObject *attr = p_class->get_attribute(CACHED_CLASS(ScriptPathAttribute));
String path = CACHED_FIELD(ScriptPathAttribute, path)->get_string_value(attr);
#ifdef TOOLS_ENABLED
scripts_metadata_filename += Engine::get_singleton()->is_editor_hint() ? "editor" : "editor_player";
#else
#ifdef DEBUG_ENABLED
scripts_metadata_filename += "debug";
#else
scripts_metadata_filename += "release";
#endif
#endif
dotnet_script_lookup_map[path] = DotNetScriptLookupInfo(
p_class->get_namespace(), p_class->get_name(), p_class);
}
String scripts_metadata_path = GodotSharpDirs::get_res_metadata_dir().plus_file(scripts_metadata_filename);
void CSharpLanguage::lookup_scripts_in_assembly(GDMonoAssembly *p_assembly) {
if (p_assembly->has_attribute(CACHED_CLASS(AssemblyHasScriptsAttribute))) {
MonoObject *attr = p_assembly->get_attribute(CACHED_CLASS(AssemblyHasScriptsAttribute));
bool requires_lookup = CACHED_FIELD(AssemblyHasScriptsAttribute, requiresLookup)->get_bool_value(attr);
if (FileAccess::exists(scripts_metadata_path)) {
String old_json;
if (requires_lookup) {
// This is supported for scenarios where specifying all types would be cumbersome,
// such as when disabling C# source generators (for whatever reason) or when using a
// language other than C# that has nothing similar to source generators to automate it.
MonoImage *image = p_assembly->get_image();
Error ferr = read_all_file_utf8(scripts_metadata_path, old_json);
int rows = mono_image_get_table_rows(image, MONO_TABLE_TYPEDEF);
ERR_FAIL_COND(ferr != OK);
for (int i = 1; i < rows; i++) {
// We don't search inner classes, only top-level.
MonoClass *mono_class = mono_class_get(image, (i + 1) | MONO_TOKEN_TYPE_DEF);
Variant old_dict_var;
String err_str;
int err_line;
Error json_err = JSON::parse(old_json, old_dict_var, err_str, err_line);
if (json_err != OK) {
ERR_PRINT("Failed to parse metadata file: '" + err_str + "' (" + String::num_int64(err_line) + ").");
return;
}
if (!mono_class_is_assignable_from(CACHED_CLASS_RAW(GodotObject), mono_class)) {
continue;
}
scripts_metadata = old_dict_var.operator Dictionary();
scripts_metadata_invalidated = false;
GDMonoClass *current = p_assembly->get_class(mono_class);
if (current) {
lookup_script_for_class(current);
}
}
} else {
// This is the most likely scenario as we use C# source generators
MonoArray *script_types = (MonoArray *)CACHED_FIELD(AssemblyHasScriptsAttribute, scriptTypes)->get_value(attr);
print_verbose("Successfully loaded scripts metadata");
} else {
if (!Engine::get_singleton()->is_editor_hint()) {
ERR_PRINT("Missing scripts metadata file.");
int length = mono_array_length(script_types);
for (int i = 0; i < length; i++) {
MonoReflectionType *reftype = mono_array_get(script_types, MonoReflectionType *, i);
ManagedType type = ManagedType::from_reftype(reftype);
ERR_CONTINUE(!type.type_class);
lookup_script_for_class(type.type_class);
}
}
}
}
@ -1300,7 +1311,7 @@ void CSharpLanguage::_on_scripts_domain_unloaded() {
}
#endif
scripts_metadata_invalidated = true;
dotnet_script_lookup_map.clear();
}
#ifdef TOOLS_ENABLED
@ -3356,45 +3367,34 @@ Error CSharpScript::reload(bool p_keep_state) {
GD_MONO_SCOPE_THREAD_ATTACH;
GDMonoAssembly *project_assembly = GDMono::get_singleton()->get_project_assembly();
const DotNetScriptLookupInfo *lookup_info =
CSharpLanguage::get_singleton()->lookup_dotnet_script(get_path());
if (project_assembly) {
const Variant *script_metadata_var = CSharpLanguage::get_singleton()->get_scripts_metadata().getptr(get_path());
if (script_metadata_var) {
Dictionary script_metadata = script_metadata_var->operator Dictionary()["class"];
const Variant *namespace_ = script_metadata.getptr("namespace");
const Variant *class_name = script_metadata.getptr("class_name");
ERR_FAIL_NULL_V(namespace_, ERR_BUG);
ERR_FAIL_NULL_V(class_name, ERR_BUG);
GDMonoClass *klass = project_assembly->get_class(namespace_->operator String(), class_name->operator String());
if (klass && CACHED_CLASS(GodotObject)->is_assignable_from(klass)) {
script_class = klass;
}
} else {
// Missing script metadata. Fallback to legacy method
script_class = project_assembly->get_object_derived_class(name);
if (lookup_info) {
GDMonoClass *klass = lookup_info->script_class;
if (klass) {
ERR_FAIL_COND_V(!CACHED_CLASS(GodotObject)->is_assignable_from(klass), FAILED);
script_class = klass;
}
valid = script_class != nullptr;
if (script_class) {
#ifdef DEBUG_ENABLED
print_verbose("Found class " + script_class->get_full_name() + " for script " + get_path());
#endif
native = GDMonoUtils::get_class_native_base(script_class);
CRASH_COND(native == nullptr);
update_script_class_info(this);
_update_exports();
}
return OK;
}
return ERR_FILE_MISSING_DEPENDENCIES;
valid = script_class != nullptr;
if (script_class) {
#ifdef DEBUG_ENABLED
print_verbose("Found class " + script_class->get_full_name() + " for script " + get_path());
#endif
native = GDMonoUtils::get_class_native_base(script_class);
CRASH_COND(native == nullptr);
update_script_class_info(this);
_update_exports();
}
return OK;
}
ScriptLanguage *CSharpScript::get_language() const {

View file

@ -66,6 +66,18 @@ TScriptInstance *cast_script_instance(ScriptInstance *p_inst) {
#define CAST_CSHARP_INSTANCE(m_inst) (cast_script_instance<CSharpInstance, CSharpLanguage>(m_inst))
struct DotNetScriptLookupInfo {
String class_namespace;
String class_name;
GDMonoClass *script_class = nullptr;
DotNetScriptLookupInfo() {} // Required by HashMap...
DotNetScriptLookupInfo(const String &p_class_namespace, const String &p_class_name, GDMonoClass *p_script_class) :
class_namespace(p_class_namespace), class_name(p_class_name), script_class(p_script_class) {
}
};
class CSharpScript : public Script {
GDCLASS(CSharpScript, Script);
@ -390,16 +402,15 @@ class CSharpLanguage : public ScriptLanguage {
int lang_idx = -1;
Dictionary scripts_metadata;
bool scripts_metadata_invalidated = true;
HashMap<String, DotNetScriptLookupInfo> dotnet_script_lookup_map;
void lookup_script_for_class(GDMonoClass *p_class);
// For debug_break and debug_break_parse
int _debug_parse_err_line = -1;
String _debug_parse_err_file;
String _debug_error;
void _load_scripts_metadata();
friend class GDMono;
void _on_scripts_domain_unloaded();
@ -436,19 +447,14 @@ public:
void reload_assemblies(bool p_soft_reload);
#endif
_FORCE_INLINE_ Dictionary get_scripts_metadata_or_nothing() {
return scripts_metadata_invalidated ? Dictionary() : scripts_metadata;
}
_FORCE_INLINE_ const Dictionary &get_scripts_metadata() {
if (scripts_metadata_invalidated) {
_load_scripts_metadata();
}
return scripts_metadata;
}
_FORCE_INLINE_ ManagedCallableMiddleman *get_managed_callable_middleman() const { return managed_callable_middleman; }
void lookup_scripts_in_assembly(GDMonoAssembly *p_assembly);
const DotNetScriptLookupInfo *lookup_dotnet_script(const String &p_script_path) const {
return dotnet_script_lookup_map.getptr(p_script_path);
}
String get_name() const override;
/* LANGUAGE FUNCTIONS */

View file

@ -2,6 +2,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.NET.Sdk", "Godot.NET.Sdk\Godot.NET.Sdk.csproj", "{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators", "Godot.SourceGenerators\Godot.SourceGenerators.csproj", "{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot.SourceGenerators.Sample", "Godot.SourceGenerators.Sample\Godot.SourceGenerators.Sample.csproj", "{7297A614-8DF5-43DE-9EAD-99671B26BD1F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GodotSharp", "..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj", "{AEBF0036-DA76-4341-B651-A3F2856AB2FA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -12,5 +18,17 @@ Global
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31B00BFA-DEA1-42FA-A472-9E54A92A8A5F}.Release|Any CPU.Build.0 = Release|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{32D31B23-2A45-4099-B4F5-95B4C8FF7D9F}.Release|Any CPU.Build.0 = Release|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7297A614-8DF5-43DE-9EAD-99671B26BD1F}.Release|Any CPU.Build.0 = Release|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AEBF0036-DA76-4341-B651-A3F2856AB2FA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View file

@ -8,43 +8,33 @@
<PackageId>Godot.NET.Sdk</PackageId>
<Version>4.0.0</Version>
<PackageProjectUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</PackageProjectUrl>
<PackageVersion>$(PackageVersion_Godot_NET_Sdk)</PackageVersion>
<RepositoryUrl>https://github.com/godotengine/godot/tree/master/modules/mono/editor/Godot.NET.Sdk</RepositoryUrl>
<PackageProjectUrl>$(RepositoryUrl)</PackageProjectUrl>
<PackageType>MSBuildSdk</PackageType>
<PackageTags>MSBuildSdk</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<!-- Exclude target framework from the package dependencies as we don't include the build output -->
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<PropertyGroup>
<NuspecFile>Godot.NET.Sdk.nuspec</NuspecFile>
<GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuSpecProperties</GenerateNuspecDependsOn>
</PropertyGroup>
<ItemGroup>
<!-- Package Sdk\Sdk.props and Sdk\Sdk.targets file -->
<None Include="Sdk\Sdk.props" Pack="true" PackagePath="Sdk" Visible="false" />
<None Include="Sdk\Sdk.targets" Pack="true" PackagePath="Sdk" Visible="false" />
<!-- SdkPackageVersions.props -->
<Target Name="ReadGodotNETSdkVersion" BeforeTargets="BeforeBuild;BeforeRebuild;CoreCompile">
<PropertyGroup>
<PackageVersion>$([System.IO.File]::ReadAllText('$(ProjectDir)Godot.NET.Sdk_PackageVersion.txt').Trim())</PackageVersion>
</PropertyGroup>
</Target>
<Target Name="SetNuSpecProperties" Condition=" Exists('$(NuspecFile)') " DependsOnTargets="ReadGodotNETSdkVersion">
<PropertyGroup>
<NuspecProperties>
id=$(PackageId);
description=$(Description);
authors=$(Authors);
version=$(PackageVersion);
packagetype=$(PackageType);
tags=$(PackageTags);
projecturl=$(PackageProjectUrl)
</NuspecProperties>
</PropertyGroup>
</Target>
<None Include="..\..\..\SdkPackageVersions.props" Pack="true" PackagePath="Sdk" Visible="false" />
</ItemGroup>
<Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack">
<PropertyGroup>
<GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath>
<GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir>
</PropertyGroup>
<Copy SourceFiles="$(OutputPath)$(PackageId).$(PackageVersion).nupkg"
DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" />
<Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" />
</Target>
</Project>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
<metadata>
<id>$id$</id>
<version>$version$</version>
<description>$description$</description>
<authors>$authors$</authors>
<owners>$authors$</owners>
<projectUrl>$projecturl$</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<licenseUrl>https://licenses.nuget.org/MIT</licenseUrl>
<tags>$tags$</tags>
<packageTypes>
<packageType name="$packagetype$" />
</packageTypes>
<repository url="$projecturl$" />
</metadata>
<files>
<file src="Sdk\**" target="Sdk" />
</files>
</package>

View file

@ -1,4 +1,6 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)\SdkPackageVersions.props" />
<PropertyGroup>
<!-- Determines if we should import Microsoft.NET.Sdk, if it wasn't already imported. -->
<GodotSdkImportsMicrosoftNetSdk Condition=" '$(UsingMicrosoftNETSdk)' != 'true' ">true</GodotSdkImportsMicrosoftNetSdk>
@ -94,6 +96,7 @@
<DefineConstants>$(GodotDefineConstants);$(DefineConstants)</DefineConstants>
</PropertyGroup>
<!-- Godot API references -->
<ItemGroup>
<!--
TODO:

View file

@ -14,4 +14,9 @@
-->
<DefineConstants Condition=" '$(GodotRealTIsDouble)' == 'true' ">GODOT_REAL_T_IS_DOUBLE;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<!-- C# source generators -->
<ItemGroup Condition=" '$(DisableImplicitGodotGeneratorReferences)' != 'true' ">
<PackageReference Include="Godot.SourceGenerators" Version="$(PackageVersion_Godot_SourceGenerators)" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,15 @@
namespace Godot.SourceGenerators.Sample
{
partial class Bar : Godot.Object
{
}
// Foo in another file
partial class Foo
{
}
partial class NotSameNameAsFile : Godot.Object
{
}
}

View file

@ -0,0 +1,11 @@
namespace Godot.SourceGenerators.Sample
{
partial class Foo : Godot.Object
{
}
// Foo again in the same file
partial class Foo
{
}
}

View file

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<!-- $(GodotProjectDir) would normally be defined by the Godot.NET.Sdk -->
<GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir>
</PropertyGroup>
<PropertyGroup>
<!-- The emitted files are not part of the compilation nor design.
They're only for peeking at the generated sources. Sometimes the
emitted files get corrupted, but that won't break anything. -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GeneratedFiles</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\glue\GodotSharp\GodotSharp\GodotSharp.csproj">
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\Godot.SourceGenerators\Godot.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<!-- This file is imported automatically when using PackageReference to
reference Godot.SourceGenerators, but not when using ProjectReference -->
<Import Project="..\Godot.SourceGenerators\Godot.SourceGenerators.props" />
</Project>

View file

@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Godot.SourceGenerators
{
public static class Common
{
public static void ReportNonPartialGodotScriptClass(
GeneratorExecutionContext context,
ClassDeclarationSyntax cds, INamedTypeSymbol symbol
)
{
string message =
"Missing partial modifier on declaration of type '" +
$"{symbol.FullQualifiedName()}' which is a subclass of '{GodotClasses.Object}'";
string description = $"{message}. Subclasses of '{GodotClasses.Object}' must be " +
"declared with the partial modifier or annotated with the " +
$"attribute '{GodotClasses.DisableGodotGeneratorsAttr}'.";
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(id: "GODOT-G0001",
title: message,
messageFormat: message,
category: "Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
description),
cds.GetLocation(),
cds.SyntaxTree.FilePath));
}
}
}

View file

@ -0,0 +1,86 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Godot.SourceGenerators
{
static class ExtensionMethods
{
public static bool TryGetGlobalAnalyzerProperty(
this GeneratorExecutionContext context, string property, out string? value
) => context.AnalyzerConfigOptions.GlobalOptions
.TryGetValue("build_property." + property, out value);
private static bool InheritsFrom(this INamedTypeSymbol? symbol, string baseName)
{
if (symbol == null)
return false;
while (true)
{
if (symbol.ToString() == baseName)
{
return true;
}
if (symbol.BaseType != null)
{
symbol = symbol.BaseType;
continue;
}
break;
}
return false;
}
private static bool IsGodotScriptClass(
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(GodotClasses.Object))
{
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.IsGodotScriptClass(compilation, out var symbol))
yield return (cds, symbol!);
}
}
public static bool IsPartial(this ClassDeclarationSyntax cds)
=> cds.Modifiers.Any(SyntaxKind.PartialKeyword);
public static bool HasDisableGeneratorsAttribute(this INamedTypeSymbol symbol)
=> symbol.GetAttributes().Any(attr =>
attr.AttributeClass?.ToString() == GodotClasses.DisableGodotGeneratorsAttr);
private static SymbolDisplayFormat FullyQualifiedFormatOmitGlobal { get; } =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted);
public static string FullQualifiedName(this INamedTypeSymbol symbol)
=> symbol.ToDisplayString(NullableFlowState.NotNull, FullyQualifiedFormatOmitGlobal);
}
}

View file

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>Core C# source generator for Godot projects.</Description>
<Authors>Godot Engine contributors</Authors>
<PackageId>Godot.SourceGenerators</PackageId>
<Version>4.0.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>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" 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="false" />
</ItemGroup>
<Target Name="CopyNupkgToSConsOutputDir" AfterTargets="Pack">
<PropertyGroup>
<GodotSourceRootPath>$(SolutionDir)\..\..\..\..\</GodotSourceRootPath>
<GodotOutputDataDir>$(GodotSourceRootPath)\bin\GodotSharp\</GodotOutputDataDir>
</PropertyGroup>
<Copy SourceFiles="$(PackageOutputPath)$(PackageId).$(PackageVersion).nupkg" DestinationFolder="$(GodotOutputDataDir)Tools\nupkgs\" />
</Target>
</Project>

View file

@ -0,0 +1,7 @@
<Project>
<ItemGroup>
<!-- $(GodotProjectDir) is defined by Godot.NET.Sdk -->
<CompilerVisibleProperty Include="GodotProjectDir" />
<CompilerVisibleProperty Include="GodotScriptPathAttributeGenerator" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,9 @@
namespace Godot.SourceGenerators
{
public static class GodotClasses
{
public const string Object = "Godot.Object";
public const string DisableGodotGeneratorsAttr = "Godot.DisableGodotGeneratorsAttribute";
public const string AssemblyHasScriptsAttr = "Godot.AssemblyHasScriptsAttribute";
}
}

View file

@ -0,0 +1,156 @@
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.TryGetGlobalAnalyzerProperty("GodotScriptPathAttributeGenerator", out string? toggle)
&& toggle == "disabled")
{
return;
}
// NOTE: IsNullOrEmpty doesn't work well with nullable checks
// ReSharper disable once ReplaceWithStringIsNullOrEmpty
if (!context.TryGetGlobalAnalyzerProperty("GodotProjectDir", out string? godotProjectDir)
|| godotProjectDir!.Length == 0)
{
throw new InvalidOperationException("Property 'GodotProjectDir' is null or empty.");
}
var godotClasses = context.Compilation.SyntaxTrees
.SelectMany(tree =>
tree.GetRoot().DescendantNodes()
.OfType<ClassDeclarationSyntax>()
// Ignore inner classes
.Where(cds => !(cds.Parent is ClassDeclarationSyntax))
.SelectGodotScriptClasses(context.Compilation)
// Report and skip non-partial classes
.Where(x =>
{
if (x.cds.IsPartial() || x.symbol.HasDisableGeneratorsAttribute())
return true;
Common.ReportNonPartialGodotScriptClass(context, x.cds, x.symbol);
return false;
})
)
// Ignore classes whose name is not the same as the file name
.Where(x => Path.GetFileNameWithoutExtension(x.cds.SyntaxTree.FilePath) == x.symbol.Name)
.GroupBy(x => x.symbol)
.ToDictionary(g => g.Key, g => g.Select(x => x.cds));
foreach (var godotClass in godotClasses)
{
VisitGodotScriptClass(context, godotProjectDir,
symbol: godotClass.Key,
classDeclarations: godotClass.Value);
}
if (godotClasses.Count <= 0)
return;
AddScriptTypesAssemblyAttr(context, godotClasses);
}
private static void VisitGodotScriptClass(
GeneratorExecutionContext context,
string godotProjectDir,
INamedTypeSymbol symbol,
IEnumerable<ClassDeclarationSyntax> classDeclarations
)
{
var attributesBuilder = 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 (attributesBuilder.Length != 0)
attributesBuilder.Append("\n ");
attributesBuilder.Append(@"[ScriptPathAttribute(""res://");
attributesBuilder.Append(RelativeToDir(cds.SyntaxTree.FilePath, godotProjectDir));
attributesBuilder.Append(@""")]");
}
string classNs = symbol.ContainingNamespace.Name;
string className = symbol.Name;
var source = $@"using Godot;
namespace {classNs}
{{
{attributesBuilder}
partial class {className}
{{
}}
}}
";
context.AddSource(classNs + "." + className + "_ScriptPath_Generated",
SourceText.From(source, 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);
if (!first)
sourceBuilder.Append(", ");
first = false;
sourceBuilder.Append("typeof(");
sourceBuilder.Append(qualifiedName);
sourceBuilder.Append(")");
}
sourceBuilder.Append("})]\n");
context.AddSource("AssemblyScriptTypes_Generated",
SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
public void Initialize(GeneratorInitializationContext context)
{
}
private static string RelativeToDir(string path, string dir)
{
// Make sure the directory ends with a path separator
dir = Path.Combine(dir, " ").TrimEnd();
if (Path.DirectorySeparatorChar == '\\')
dir = dir.Replace("/", "\\") + "\\";
var fullPath = new Uri(Path.GetFullPath(path), UriKind.Absolute);
var relRoot = new Uri(Path.GetFullPath(dir), UriKind.Absolute);
// MakeRelativeUri converts spaces to %20, hence why we need UnescapeDataString
return Uri.UnescapeDataString(relRoot.MakeRelativeUri(fullPath).ToString());
}
}
}

View file

@ -1,11 +1,5 @@
using System;
using GodotTools.Core;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Microsoft.Build.Construction;
using Microsoft.Build.Globbing;
namespace GodotTools.ProjectEditor
{
@ -31,47 +25,6 @@ namespace GodotTools.ProjectEditor
return root != null ? new MSBuildProject(root) : null;
}
private static List<string> GetAllFilesRecursive(string rootDirectory, string mask)
{
string[] files = Directory.GetFiles(rootDirectory, mask, SearchOption.AllDirectories);
// We want relative paths
for (int i = 0; i < files.Length; i++)
{
files[i] = files[i].RelativeToPath(rootDirectory);
}
return new List<string>(files);
}
// NOTE: Assumes auto-including items. Only used by the scripts metadata generator, which will be replaced with source generators in the future.
public static IEnumerable<string> GetIncludeFiles(string projectPath, string itemType)
{
var excluded = new List<string>();
var includedFiles = GetAllFilesRecursive(Path.GetDirectoryName(projectPath), "*.cs");
var root = ProjectRootElement.Open(projectPath);
Debug.Assert(root != null);
foreach (var item in root.Items)
{
if (string.IsNullOrEmpty(item.Condition))
continue;
if (item.ItemType != itemType)
continue;
string normalizedRemove = item.Remove.NormalizePath();
var glob = MSBuildGlob.Parse(normalizedRemove);
excluded.AddRange(includedFiles.Where(includedFile => glob.IsMatch(includedFile)));
}
includedFiles.RemoveAll(f => excluded.Contains(f));
return includedFiles;
}
public static void MigrateToProjectSdksStyle(MSBuildProject project, string projectName)
{
var origRoot = project.Root;

View file

@ -3,7 +3,6 @@
<Target Name="SetPropertiesForGenerateGodotNupkgsVersions">
<PropertyGroup>
<GodotNETSdkPackageVersionFile>$(SolutionDir)..\Godot.NET.Sdk\Godot.NET.Sdk\Godot.NET.Sdk_PackageVersion.txt</GodotNETSdkPackageVersionFile>
<GeneratedGodotNupkgsVersionsFile>$(IntermediateOutputPath)GodotNupkgsVersions.g.cs</GeneratedGodotNupkgsVersionsFile>
</PropertyGroup>
</Target>
@ -18,13 +17,14 @@
</Target>
<Target Name="_GenerateGodotNupkgsVersionsFile"
DependsOnTargets="SetPropertiesForGenerateGodotNupkgsVersions"
Inputs="$(MSBuildProjectFile);@(GodotNETSdkPackageVersionFile)"
Inputs="$(MSBuildProjectFile);$(MSBuildThisFileDirectory);$(MSBuildProjectFile)\..\..\..\SdkPackageVersions.props"
Outputs="$(GeneratedGodotNupkgsVersionsFile)">
<PropertyGroup>
<GenerateGodotNupkgsVersionsCode><![CDATA[
namespace $(RootNamespace) {
public class GeneratedGodotNupkgsVersions {
public const string GodotNETSdk = "$([System.IO.File]::ReadAllText('$(GodotNETSdkPackageVersionFile)').Trim())"%3b
public const string GodotNETSdk = "$(PackageVersion_Godot_NET_Sdk)"%3b
public const string GodotSourceGenerators = "$(PackageVersion_Godot_SourceGenerators)"%3b
}
}
]]></GenerateGodotNupkgsVersionsCode>

View file

@ -218,43 +218,12 @@ namespace GodotTools.Build
Godot.GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message);
}
GenerateEditorScriptMetadata();
if (GodotSharpEditor.Instance.SkipBuildBeforePlaying)
return true; // Requested play from an external editor/IDE which already built the project
return BuildProjectBlocking("Debug");
}
// NOTE: This will be replaced with C# source generators in 4.0
public static void GenerateEditorScriptMetadata()
{
string editorScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor");
string playerScriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, "scripts_metadata.editor_player");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, editorScriptsMetadataPath);
if (!File.Exists(editorScriptsMetadataPath))
return;
try
{
File.Copy(editorScriptsMetadataPath, playerScriptsMetadataPath);
}
catch (IOException e)
{
throw new IOException("Failed to copy scripts metadata file.", innerException: e);
}
}
// NOTE: This will be replaced with C# source generators in 4.0
public static string GenerateExportedGameScriptMetadata(bool isDebug)
{
string scriptsMetadataPath = Path.Combine(GodotSharpDirs.ResMetadataDir, $"scripts_metadata.{(isDebug ? "debug" : "release")}");
CsProjOperations.GenerateScriptsMetadata(GodotSharpDirs.ProjectCsProjPath, scriptsMetadataPath);
return scriptsMetadataPath;
}
public static void Initialize()
{
// Build tool settings

View file

@ -43,8 +43,6 @@ namespace GodotTools.Build
GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message);
}
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug"))
return; // Build failed
@ -74,8 +72,6 @@ namespace GodotTools.Build
GD.PushError("Failed to setup Godot NuGet Offline Packages: " + e.Message);
}
BuildManager.GenerateEditorScriptMetadata();
if (!BuildManager.BuildProjectBlocking("Debug", targets: new[] {"Rebuild"}))
return; // Build failed

View file

@ -290,7 +290,8 @@ namespace GodotTools.Build
private static readonly (string packageId, string packageVersion)[] PackagesToAdd =
{
("Godot.NET.Sdk", GeneratedGodotNupkgsVersions.GodotNETSdk)
("Godot.NET.Sdk", GeneratedGodotNupkgsVersions.GodotNETSdk),
("Godot.SourceGenerators", GeneratedGodotNupkgsVersions.GodotSourceGenerators),
};
}
}

View file

@ -1,11 +1,6 @@
using Godot;
using System;
using System.Linq;
using Godot.Collections;
using GodotTools.Internals;
using GodotTools.ProjectEditor;
using File = GodotTools.Utils.File;
using Directory = GodotTools.Utils.Directory;
namespace GodotTools
{
@ -23,86 +18,5 @@ namespace GodotTools
return string.Empty;
}
}
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static ulong ConvertToTimestamp(this DateTime value)
{
TimeSpan elapsedTime = value - Epoch;
return (ulong)elapsedTime.TotalSeconds;
}
private static bool TryParseFileMetadata(string includeFile, ulong modifiedTime, out Dictionary fileMetadata)
{
fileMetadata = null;
var parseError = ScriptClassParser.ParseFile(includeFile, out var classes, out string errorStr);
if (parseError != Error.Ok)
{
GD.PushError($"Failed to determine namespace and class for script: {includeFile}. Parse error: {errorStr ?? parseError.ToString()}");
return false;
}
string searchName = System.IO.Path.GetFileNameWithoutExtension(includeFile);
var firstMatch = classes.FirstOrDefault(classDecl =>
classDecl.BaseCount != 0 && // If it doesn't inherit anything, it can't be a Godot.Object.
classDecl.SearchName == searchName // Filter by the name we're looking for
);
if (firstMatch == null)
return false; // Not found
fileMetadata = new Dictionary
{
["modified_time"] = $"{modifiedTime}",
["class"] = new Dictionary
{
["namespace"] = firstMatch.Namespace,
["class_name"] = firstMatch.Name,
["nested"] = firstMatch.Nested
}
};
return true;
}
public static void GenerateScriptsMetadata(string projectPath, string outputPath)
{
var metadataDict = Internal.GetScriptsMetadataOrNothing().Duplicate();
bool IsUpToDate(string includeFile, ulong modifiedTime)
{
return metadataDict.TryGetValue(includeFile, out var oldFileVar) &&
ulong.TryParse(((Dictionary)oldFileVar)["modified_time"] as string,
out ulong storedModifiedTime) && storedModifiedTime == modifiedTime;
}
var outdatedFiles = ProjectUtils.GetIncludeFiles(projectPath, "Compile")
.Select(path => ("res://" + path).SimplifyGodotPath())
.ToDictionary(path => path, path => File.GetLastWriteTime(path).ConvertToTimestamp())
.Where(pair => !IsUpToDate(includeFile: pair.Key, modifiedTime: pair.Value))
.ToArray();
foreach (var pair in outdatedFiles)
{
metadataDict.Remove(pair.Key);
string includeFile = pair.Key;
if (TryParseFileMetadata(includeFile, modifiedTime: pair.Value, out var fileMetadata))
metadataDict[includeFile] = fileMetadata;
}
string json = metadataDict.Count <= 0 ? "{}" : JSON.Print(metadataDict);
string baseDir = outputPath.GetBaseDir();
if (!Directory.Exists(baseDir))
Directory.CreateDirectory(baseDir);
File.WriteAllText(outputPath, json);
}
}
}

View file

@ -157,9 +157,6 @@ namespace GodotTools.Export
string buildConfig = isDebug ? "ExportDebug" : "ExportRelease";
string scriptsMetadataPath = BuildManager.GenerateExportedGameScriptMetadata(isDebug);
AddFile(scriptsMetadataPath, scriptsMetadataPath);
if (!BuildManager.BuildProjectBlocking(buildConfig, platform: platform))
throw new Exception("Failed to build project");

View file

@ -1,7 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
using GodotTools.IdeMessaging.Requests;
namespace GodotTools.Internals
@ -42,9 +40,6 @@ namespace GodotTools.Internals
public static void EditorNodeShowScriptScreen() => internal_EditorNodeShowScriptScreen();
public static Dictionary<string, object> GetScriptsMetadataOrNothing() =>
internal_GetScriptsMetadataOrNothing(typeof(Dictionary<string, object>));
public static string MonoWindowsInstallRoot => internal_MonoWindowsInstallRoot();
public static void EditorRunPlay() => internal_EditorRunPlay();
@ -100,9 +95,6 @@ namespace GodotTools.Internals
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void internal_EditorNodeShowScriptScreen();
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern Dictionary<string, object> internal_GetScriptsMetadataOrNothing(Type dictType);
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string internal_MonoWindowsInstallRoot();

View file

@ -1,61 +0,0 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Godot;
using Godot.Collections;
namespace GodotTools.Internals
{
public static class ScriptClassParser
{
public class ClassDecl
{
public string Name { get; }
public string Namespace { get; }
public bool Nested { get; }
public long BaseCount { get; }
public string SearchName => Nested ?
Name.Substring(Name.LastIndexOf(".", StringComparison.Ordinal) + 1) :
Name;
public ClassDecl(string name, string @namespace, bool nested, long baseCount)
{
Name = name;
Namespace = @namespace;
Nested = nested;
BaseCount = baseCount;
}
}
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern Error internal_ParseFile(string filePath, Array<Dictionary> classes, out string errorStr);
public static Error ParseFile(string filePath, out IEnumerable<ClassDecl> classes, out string errorStr)
{
var classesArray = new Array<Dictionary>();
var error = internal_ParseFile(filePath, classesArray, out errorStr);
if (error != Error.Ok)
{
classes = null;
return error;
}
var classesList = new List<ClassDecl>();
foreach (var classDeclDict in classesArray)
{
classesList.Add(new ClassDecl(
(string)classDeclDict["name"],
(string)classDeclDict["namespace"],
(bool)classDeclDict["nested"],
(long)classDeclDict["base_count"]
));
}
classes = classesList;
return Error.Ok;
}
}
}

View file

@ -49,7 +49,6 @@
#include "../utils/osx_utils.h"
#include "code_completion.h"
#include "godotsharp_export.h"
#include "script_class_parser.h"
MonoString *godot_icall_GodotSharpDirs_ResDataDir() {
return GDMonoMarshal::mono_string_from_godot(GodotSharpDirs::get_res_data_dir());
@ -172,36 +171,6 @@ MonoBoolean godot_icall_EditorProgress_Step(MonoString *p_task, MonoString *p_st
return EditorNode::progress_task_step(task, state, p_step, (bool)p_force_refresh);
}
int32_t godot_icall_ScriptClassParser_ParseFile(MonoString *p_filepath, MonoObject *p_classes, MonoString **r_error_str) {
*r_error_str = nullptr;
String filepath = GDMonoMarshal::mono_string_to_godot(p_filepath);
ScriptClassParser scp;
Error err = scp.parse_file(filepath);
if (err == OK) {
Array classes = GDMonoMarshal::mono_object_to_variant(p_classes);
const Vector<ScriptClassParser::ClassDecl> &class_decls = scp.get_classes();
for (int i = 0; i < class_decls.size(); i++) {
const ScriptClassParser::ClassDecl &classDecl = class_decls[i];
Dictionary classDeclDict;
classDeclDict["name"] = classDecl.name;
classDeclDict["namespace"] = classDecl.namespace_;
classDeclDict["nested"] = classDecl.nested;
classDeclDict["base_count"] = classDecl.base.size();
classes.push_back(classDeclDict);
}
} else {
String error_str = scp.get_error();
if (!error_str.is_empty()) {
*r_error_str = GDMonoMarshal::mono_string_from_godot(error_str);
}
}
return err;
}
uint32_t godot_icall_ExportPlugin_GetExportedAssemblyDependencies(MonoObject *p_initial_assemblies,
MonoString *p_build_config, MonoString *p_custom_bcl_dir, MonoObject *r_assembly_dependencies) {
Dictionary initial_dependencies = GDMonoMarshal::mono_object_to_variant(p_initial_assemblies);
@ -289,18 +258,6 @@ void godot_icall_Internal_EditorNodeShowScriptScreen() {
EditorNode::get_singleton()->call("_editor_select", EditorNode::EDITOR_SCRIPT);
}
MonoObject *godot_icall_Internal_GetScriptsMetadataOrNothing(MonoReflectionType *p_dict_reftype) {
Dictionary maybe_metadata = CSharpLanguage::get_singleton()->get_scripts_metadata_or_nothing();
MonoType *dict_type = mono_reflection_type_get_type(p_dict_reftype);
int type_encoding = mono_type_get_type(dict_type);
MonoClass *type_class_raw = mono_class_from_mono_type(dict_type);
GDMonoClass *type_class = GDMono::get_singleton()->get_class(type_class_raw);
return GDMonoMarshal::variant_to_mono_object(maybe_metadata, ManagedType(type_encoding, type_class));
}
MonoString *godot_icall_Internal_MonoWindowsInstallRoot() {
#ifdef WINDOWS_ENABLED
String install_root_dir = GDMono::get_singleton()->get_mono_reg_info().install_root_dir;
@ -395,9 +352,6 @@ void register_editor_internal_calls() {
GDMonoUtils::add_internal_call("GodotTools.Internals.EditorProgress::internal_Dispose", godot_icall_EditorProgress_Dispose);
GDMonoUtils::add_internal_call("GodotTools.Internals.EditorProgress::internal_Step", godot_icall_EditorProgress_Step);
// ScriptClassParser
GDMonoUtils::add_internal_call("GodotTools.Internals.ScriptClassParser::internal_ParseFile", godot_icall_ScriptClassParser_ParseFile);
// ExportPlugin
GDMonoUtils::add_internal_call("GodotTools.Export.ExportPlugin::internal_GetExportedAssemblyDependencies", godot_icall_ExportPlugin_GetExportedAssemblyDependencies);
@ -416,7 +370,6 @@ void register_editor_internal_calls() {
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorDebuggerNodeReloadScripts", godot_icall_Internal_EditorDebuggerNodeReloadScripts);
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_ScriptEditorEdit", godot_icall_Internal_ScriptEditorEdit);
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorNodeShowScriptScreen", godot_icall_Internal_EditorNodeShowScriptScreen);
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_GetScriptsMetadataOrNothing", godot_icall_Internal_GetScriptsMetadataOrNothing);
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_MonoWindowsInstallRoot", godot_icall_Internal_MonoWindowsInstallRoot);
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorRunPlay", godot_icall_Internal_EditorRunPlay);
GDMonoUtils::add_internal_call("GodotTools.Internals.Internal::internal_EditorRunStop", godot_icall_Internal_EditorRunStop);

View file

@ -1,753 +0,0 @@
/*************************************************************************/
/* script_class_parser.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#include "script_class_parser.h"
#include "core/os/os.h"
#include "core/templates/map.h"
#include "../utils/string_utils.h"
const char *ScriptClassParser::token_names[ScriptClassParser::TK_MAX] = {
"[",
"]",
"{",
"}",
".",
":",
",",
"Symbol",
"Identifier",
"String",
"Number",
"<",
">",
"EOF",
"Error"
};
String ScriptClassParser::get_token_name(ScriptClassParser::Token p_token) {
ERR_FAIL_INDEX_V(p_token, TK_MAX, "<error>");
return token_names[p_token];
}
ScriptClassParser::Token ScriptClassParser::get_token() {
while (true) {
switch (code[idx]) {
case '\n': {
line++;
idx++;
break;
};
case 0: {
return TK_EOF;
} break;
case '{': {
idx++;
return TK_CURLY_BRACKET_OPEN;
};
case '}': {
idx++;
return TK_CURLY_BRACKET_CLOSE;
};
case '[': {
idx++;
return TK_BRACKET_OPEN;
};
case ']': {
idx++;
return TK_BRACKET_CLOSE;
};
case '<': {
idx++;
return TK_OP_LESS;
};
case '>': {
idx++;
return TK_OP_GREATER;
};
case ':': {
idx++;
return TK_COLON;
};
case ',': {
idx++;
return TK_COMMA;
};
case '.': {
idx++;
return TK_PERIOD;
};
case '#': {
//compiler directive
while (code[idx] != '\n' && code[idx] != 0) {
idx++;
}
continue;
} break;
case '/': {
switch (code[idx + 1]) {
case '*': { // block comment
idx += 2;
while (true) {
if (code[idx] == 0) {
error_str = "Unterminated comment";
error = true;
return TK_ERROR;
} else if (code[idx] == '*' && code[idx + 1] == '/') {
idx += 2;
break;
} else if (code[idx] == '\n') {
line++;
}
idx++;
}
} break;
case '/': { // line comment skip
while (code[idx] != '\n' && code[idx] != 0) {
idx++;
}
} break;
default: {
value = "/";
idx++;
return TK_SYMBOL;
}
}
continue; // a comment
} break;
case '\'':
case '"': {
bool verbatim = idx != 0 && code[idx - 1] == '@';
char32_t begin_str = code[idx];
idx++;
String tk_string = String();
while (true) {
if (code[idx] == 0) {
error_str = "Unterminated String";
error = true;
return TK_ERROR;
} else if (code[idx] == begin_str) {
if (verbatim && code[idx + 1] == '"') { // '""' is verbatim string's '\"'
idx += 2; // skip next '"' as well
continue;
}
idx += 1;
break;
} else if (code[idx] == '\\' && !verbatim) {
//escaped characters...
idx++;
char32_t next = code[idx];
if (next == 0) {
error_str = "Unterminated String";
error = true;
return TK_ERROR;
}
char32_t res = 0;
switch (next) {
case 'b':
res = 8;
break;
case 't':
res = 9;
break;
case 'n':
res = 10;
break;
case 'f':
res = 12;
break;
case 'r':
res = 13;
break;
case '\"':
res = '\"';
break;
case '\\':
res = '\\';
break;
default: {
res = next;
} break;
}
tk_string += res;
} else {
if (code[idx] == '\n') {
line++;
}
tk_string += code[idx];
}
idx++;
}
value = tk_string;
return TK_STRING;
} break;
default: {
if (code[idx] <= 32) {
idx++;
break;
}
if ((code[idx] >= 33 && code[idx] <= 47) || (code[idx] >= 58 && code[idx] <= 63) || (code[idx] >= 91 && code[idx] <= 94) || code[idx] == 96 || (code[idx] >= 123 && code[idx] <= 127)) {
value = String::chr(code[idx]);
idx++;
return TK_SYMBOL;
}
if (code[idx] == '-' || (code[idx] >= '0' && code[idx] <= '9')) {
//a number
const char32_t *rptr;
double number = String::to_float(&code[idx], &rptr);
idx += (rptr - &code[idx]);
value = number;
return TK_NUMBER;
} else if ((code[idx] == '@' && code[idx + 1] != '"') || code[idx] == '_' || (code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || code[idx] > 127) {
String id;
id += code[idx];
idx++;
while (code[idx] == '_' || (code[idx] >= 'A' && code[idx] <= 'Z') || (code[idx] >= 'a' && code[idx] <= 'z') || (code[idx] >= '0' && code[idx] <= '9') || code[idx] > 127) {
id += code[idx];
idx++;
}
value = id;
return TK_IDENTIFIER;
} else if (code[idx] == '@' && code[idx + 1] == '"') {
// begin of verbatim string
idx++;
} else {
error_str = "Unexpected character.";
error = true;
return TK_ERROR;
}
}
}
}
}
Error ScriptClassParser::_skip_generic_type_params() {
Token tk;
while (true) {
tk = get_token();
if (tk == TK_IDENTIFIER) {
tk = get_token();
// Type specifications can end with "?" to denote nullable types, such as IList<int?>
if (tk == TK_SYMBOL) {
tk = get_token();
if (value.operator String() != "?") {
error_str = "Expected " + get_token_name(TK_IDENTIFIER) + ", found unexpected symbol '" + value + "'";
error = true;
return ERR_PARSE_ERROR;
}
if (tk != TK_OP_GREATER && tk != TK_COMMA) {
error_str = "Nullable type symbol '?' is only allowed after an identifier, but found " + get_token_name(tk) + " next.";
error = true;
return ERR_PARSE_ERROR;
}
}
if (tk == TK_PERIOD) {
while (true) {
tk = get_token();
if (tk != TK_IDENTIFIER) {
error_str = "Expected " + get_token_name(TK_IDENTIFIER) + ", found: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
tk = get_token();
if (tk != TK_PERIOD) {
break;
}
}
}
if (tk == TK_OP_LESS) {
Error err = _skip_generic_type_params();
if (err) {
return err;
}
tk = get_token();
}
if (tk == TK_OP_GREATER) {
return OK;
} else if (tk != TK_COMMA) {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
} else if (tk == TK_OP_LESS) {
error_str = "Expected " + get_token_name(TK_IDENTIFIER) + ", found " + get_token_name(TK_OP_LESS);
error = true;
return ERR_PARSE_ERROR;
} else if (tk == TK_OP_GREATER) {
return OK;
} else {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
}
}
Error ScriptClassParser::_parse_type_full_name(String &r_full_name) {
Token tk = get_token();
if (tk != TK_IDENTIFIER) {
error_str = "Expected " + get_token_name(TK_IDENTIFIER) + ", found: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
r_full_name += String(value);
if (code[idx] == '<') {
idx++;
// We don't mind if the base is generic, but we skip it any ways since this information is not needed
Error err = _skip_generic_type_params();
if (err) {
return err;
}
}
if (code[idx] != '.') { // We only want to take the next token if it's a period
return OK;
}
tk = get_token();
CRASH_COND(tk != TK_PERIOD); // Assertion
r_full_name += ".";
return _parse_type_full_name(r_full_name);
}
Error ScriptClassParser::_parse_class_base(Vector<String> &r_base) {
String name;
Error err = _parse_type_full_name(name);
if (err) {
return err;
}
Token tk = get_token();
if (tk == TK_COMMA) {
err = _parse_class_base(r_base);
if (err) {
return err;
}
} else if (tk == TK_IDENTIFIER && String(value) == "where") {
err = _parse_type_constraints();
if (err) {
return err;
}
// An open curly bracket was parsed by _parse_type_constraints, so we can exit
} else if (tk == TK_CURLY_BRACKET_OPEN) {
// we are finished when we hit the open curly bracket
} else {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
r_base.push_back(name);
return OK;
}
Error ScriptClassParser::_parse_type_constraints() {
Token tk = get_token();
if (tk != TK_IDENTIFIER) {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
tk = get_token();
if (tk != TK_COLON) {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
while (true) {
tk = get_token();
if (tk == TK_IDENTIFIER) {
if (String(value) == "where") {
return _parse_type_constraints();
}
tk = get_token();
if (tk == TK_PERIOD) {
while (true) {
tk = get_token();
if (tk != TK_IDENTIFIER) {
error_str = "Expected " + get_token_name(TK_IDENTIFIER) + ", found: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
tk = get_token();
if (tk != TK_PERIOD) {
break;
}
}
}
}
if (tk == TK_COMMA) {
continue;
} else if (tk == TK_IDENTIFIER && String(value) == "where") {
return _parse_type_constraints();
} else if (tk == TK_SYMBOL && String(value) == "(") {
tk = get_token();
if (tk != TK_SYMBOL || String(value) != ")") {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
} else if (tk == TK_OP_LESS) {
Error err = _skip_generic_type_params();
if (err) {
return err;
}
} else if (tk == TK_CURLY_BRACKET_OPEN) {
return OK;
} else {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
}
}
Error ScriptClassParser::_parse_namespace_name(String &r_name, int &r_curly_stack) {
Token tk = get_token();
if (tk == TK_IDENTIFIER) {
r_name += String(value);
} else {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
tk = get_token();
if (tk == TK_PERIOD) {
r_name += ".";
return _parse_namespace_name(r_name, r_curly_stack);
} else if (tk == TK_CURLY_BRACKET_OPEN) {
r_curly_stack++;
return OK;
} else {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
}
Error ScriptClassParser::parse(const String &p_code) {
code = p_code;
idx = 0;
line = 0;
error_str = String();
error = false;
value = Variant();
classes.clear();
Token tk = get_token();
Map<int, NameDecl> name_stack;
int curly_stack = 0;
int type_curly_stack = 0;
while (!error && tk != TK_EOF) {
String identifier = value;
if (tk == TK_IDENTIFIER && (identifier == "class" || identifier == "struct")) {
bool is_class = identifier == "class";
tk = get_token();
if (tk == TK_IDENTIFIER) {
String name = value;
int at_level = curly_stack;
ClassDecl class_decl;
for (Map<int, NameDecl>::Element *E = name_stack.front(); E; E = E->next()) {
const NameDecl &name_decl = E->value();
if (name_decl.type == NameDecl::NAMESPACE_DECL) {
if (E != name_stack.front()) {
class_decl.namespace_ += ".";
}
class_decl.namespace_ += name_decl.name;
} else {
class_decl.name += name_decl.name + ".";
}
}
class_decl.name += name;
class_decl.nested = type_curly_stack > 0;
bool generic = false;
while (true) {
tk = get_token();
if (tk == TK_COLON) {
Error err = _parse_class_base(class_decl.base);
if (err) {
return err;
}
curly_stack++;
type_curly_stack++;
break;
} else if (tk == TK_CURLY_BRACKET_OPEN) {
curly_stack++;
type_curly_stack++;
break;
} else if (tk == TK_OP_LESS && !generic) {
generic = true;
Error err = _skip_generic_type_params();
if (err) {
return err;
}
} else if (tk == TK_IDENTIFIER && String(value) == "where") {
Error err = _parse_type_constraints();
if (err) {
return err;
}
// An open curly bracket was parsed by _parse_type_constraints, so we can exit
curly_stack++;
type_curly_stack++;
break;
} else {
error_str = "Unexpected token: " + get_token_name(tk);
error = true;
return ERR_PARSE_ERROR;
}
}
NameDecl name_decl;
name_decl.name = name;
name_decl.type = is_class ? NameDecl::CLASS_DECL : NameDecl::STRUCT_DECL;
name_stack[at_level] = name_decl;
if (is_class) {
if (!generic) { // no generics, thanks
classes.push_back(class_decl);
} else if (OS::get_singleton()->is_stdout_verbose()) {
String full_name = class_decl.namespace_;
if (full_name.length()) {
full_name += ".";
}
full_name += class_decl.name;
OS::get_singleton()->print("Ignoring generic class declaration: %s\n", full_name.utf8().get_data());
}
}
}
} else if (tk == TK_IDENTIFIER && identifier == "namespace") {
if (type_curly_stack > 0) {
error_str = "Found namespace nested inside type.";
error = true;
return ERR_PARSE_ERROR;
}
String name;
int at_level = curly_stack;
Error err = _parse_namespace_name(name, curly_stack);
if (err) {
return err;
}
NameDecl name_decl;
name_decl.name = name;
name_decl.type = NameDecl::NAMESPACE_DECL;
name_stack[at_level] = name_decl;
} else if (tk == TK_CURLY_BRACKET_OPEN) {
curly_stack++;
} else if (tk == TK_CURLY_BRACKET_CLOSE) {
curly_stack--;
if (name_stack.has(curly_stack)) {
if (name_stack[curly_stack].type != NameDecl::NAMESPACE_DECL) {
type_curly_stack--;
}
name_stack.erase(curly_stack);
}
}
tk = get_token();
}
if (!error && tk == TK_EOF && curly_stack > 0) {
error_str = "Reached EOF with missing close curly brackets.";
error = true;
}
if (error) {
return ERR_PARSE_ERROR;
}
return OK;
}
static String get_preprocessor_directive(const String &p_line, int p_from) {
CRASH_COND(p_line[p_from] != '#');
p_from++;
int i = p_from;
while (i < p_line.length() && (p_line[i] == '_' || (p_line[i] >= 'A' && p_line[i] <= 'Z') ||
(p_line[i] >= 'a' && p_line[i] <= 'z') || p_line[i] > 127)) {
i++;
}
return p_line.substr(p_from, i - p_from);
}
static void run_dummy_preprocessor(String &r_source, const String &p_filepath) {
Vector<String> lines = r_source.split("\n", /* p_allow_empty: */ true);
bool *include_lines = memnew_arr(bool, lines.size());
int if_level = -1;
Vector<bool> is_branch_being_compiled;
for (int i = 0; i < lines.size(); i++) {
const String &line = lines[i];
const int line_len = line.length();
int j;
for (j = 0; j < line_len; j++) {
if (line[j] != ' ' && line[j] != '\t') {
if (line[j] == '#') {
// First non-whitespace char of the line is '#'
include_lines[i] = false;
String directive = get_preprocessor_directive(line, j);
if (directive == "if") {
if_level++;
is_branch_being_compiled.push_back(if_level == 0 || is_branch_being_compiled[if_level - 1]);
} else if (directive == "elif") {
ERR_CONTINUE_MSG(if_level == -1, "Found unexpected '#elif' directive. File: '" + p_filepath + "'.");
is_branch_being_compiled.write[if_level] = false;
} else if (directive == "else") {
ERR_CONTINUE_MSG(if_level == -1, "Found unexpected '#else' directive. File: '" + p_filepath + "'.");
is_branch_being_compiled.write[if_level] = false;
} else if (directive == "endif") {
ERR_CONTINUE_MSG(if_level == -1, "Found unexpected '#endif' directive. File: '" + p_filepath + "'.");
is_branch_being_compiled.remove(if_level);
if_level--;
}
break;
} else {
// First non-whitespace char of the line is not '#'
include_lines[i] = if_level == -1 || is_branch_being_compiled[if_level];
break;
}
}
}
if (j == line_len) {
// Loop ended without finding a non-whitespace character.
// Either the line was empty or it only contained whitespaces.
include_lines[i] = if_level == -1 || is_branch_being_compiled[if_level];
}
}
r_source.clear();
// Custom join ignoring lines removed by the preprocessor
for (int i = 0; i < lines.size(); i++) {
if (i > 0 && include_lines[i - 1]) {
r_source += '\n';
}
if (include_lines[i]) {
r_source += lines[i];
}
}
}
Error ScriptClassParser::parse_file(const String &p_filepath) {
String source;
Error ferr = read_all_file_utf8(p_filepath, source);
ERR_FAIL_COND_V_MSG(ferr != OK, ferr,
ferr == ERR_INVALID_DATA ?
"File '" + p_filepath + "' contains invalid unicode (UTF-8), so it was not loaded."
" Please ensure that scripts are saved in valid UTF-8 unicode." :
"Failed to read file: '" + p_filepath + "'.");
run_dummy_preprocessor(source, p_filepath);
return parse(source);
}
String ScriptClassParser::get_error() {
return error_str;
}
Vector<ScriptClassParser::ClassDecl> ScriptClassParser::get_classes() {
return classes;
}

View file

@ -1,108 +0,0 @@
/*************************************************************************/
/* script_class_parser.h */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
#ifndef SCRIPT_CLASS_PARSER_H
#define SCRIPT_CLASS_PARSER_H
#include "core/string/ustring.h"
#include "core/templates/vector.h"
#include "core/variant/variant.h"
class ScriptClassParser {
public:
struct NameDecl {
enum Type {
NAMESPACE_DECL,
CLASS_DECL,
STRUCT_DECL
};
String name;
Type type = NAMESPACE_DECL;
};
struct ClassDecl {
String name;
String namespace_;
Vector<String> base;
bool nested = false;
};
private:
String code;
int idx = 0;
int line = 0;
String error_str;
bool error = false;
Variant value;
Vector<ClassDecl> classes;
enum Token {
TK_BRACKET_OPEN,
TK_BRACKET_CLOSE,
TK_CURLY_BRACKET_OPEN,
TK_CURLY_BRACKET_CLOSE,
TK_PERIOD,
TK_COLON,
TK_COMMA,
TK_SYMBOL,
TK_IDENTIFIER,
TK_STRING,
TK_NUMBER,
TK_OP_LESS,
TK_OP_GREATER,
TK_EOF,
TK_ERROR,
TK_MAX
};
static const char *token_names[TK_MAX];
static String get_token_name(Token p_token);
Token get_token();
Error _skip_generic_type_params();
Error _parse_type_full_name(String &r_full_name);
Error _parse_class_base(Vector<String> &r_base);
Error _parse_type_constraints();
Error _parse_namespace_name(String &r_name, int &r_curly_stack);
public:
Error parse(const String &p_code);
Error parse_file(const String &p_filepath);
String get_error();
Vector<ClassDecl> get_classes();
};
#endif // SCRIPT_CLASS_PARSER_H

View file

@ -0,0 +1,22 @@
using System;
namespace Godot
{
[AttributeUsage(AttributeTargets.Assembly)]
public class AssemblyHasScriptsAttribute : Attribute
{
private readonly bool requiresLookup;
private readonly System.Type[] scriptTypes;
public AssemblyHasScriptsAttribute()
{
requiresLookup = true;
}
public AssemblyHasScriptsAttribute(System.Type[] scriptTypes)
{
requiresLookup = false;
this.scriptTypes = scriptTypes;
}
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace Godot
{
[AttributeUsage(AttributeTargets.Class)]
public class DisableGodotGeneratorsAttribute : Attribute
{
}
}

View file

@ -0,0 +1,15 @@
using System;
namespace Godot
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class ScriptPathAttribute : Attribute
{
private string path;
public ScriptPathAttribute(string path)
{
this.path = path;
}
}
}

View file

@ -14,9 +14,12 @@
<ItemGroup>
<Compile Include="Core\AABB.cs" />
<Compile Include="Core\Array.cs" />
<Compile Include="Core\Attributes\AssemblyHasScriptsAttribute.cs" />
<Compile Include="Core\Attributes\DisableGodotGeneratorsAttribute.cs" />
<Compile Include="Core\Attributes\ExportAttribute.cs" />
<Compile Include="Core\Attributes\GodotMethodAttribute.cs" />
<Compile Include="Core\Attributes\RPCAttributes.cs" />
<Compile Include="Core\Attributes\ScriptPathAttribute.cs" />
<Compile Include="Core\Attributes\SignalAttribute.cs" />
<Compile Include="Core\Attributes\ToolAttribute.cs" />
<Compile Include="Core\Basis.cs" />

View file

@ -1006,6 +1006,7 @@ bool GDMono::_load_project_assembly() {
if (success) {
mono_assembly_set_main(project_assembly->get_assembly());
CSharpLanguage::get_singleton()->lookup_scripts_in_assembly(project_assembly);
}
return success;

View file

@ -345,6 +345,45 @@ String GDMonoAssembly::get_path() const {
return String::utf8(mono_image_get_filename(image));
}
bool GDMonoAssembly::has_attribute(GDMonoClass *p_attr_class) {
#ifdef DEBUG_ENABLED
ERR_FAIL_NULL_V(p_attr_class, false);
#endif
if (!attrs_fetched) {
fetch_attributes();
}
if (!attributes) {
return false;
}
return mono_custom_attrs_has_attr(attributes, p_attr_class->get_mono_ptr());
}
MonoObject *GDMonoAssembly::get_attribute(GDMonoClass *p_attr_class) {
#ifdef DEBUG_ENABLED
ERR_FAIL_NULL_V(p_attr_class, nullptr);
#endif
if (!attrs_fetched) {
fetch_attributes();
}
if (!attributes) {
return nullptr;
}
return mono_custom_attrs_get_attr(attributes, p_attr_class->get_mono_ptr());
}
void GDMonoAssembly::fetch_attributes() {
ERR_FAIL_COND(attributes != nullptr);
attributes = mono_custom_attrs_from_assembly(assembly);
attrs_fetched = true;
}
GDMonoClass *GDMonoAssembly::get_class(const StringName &p_namespace, const StringName &p_name) {
ERR_FAIL_NULL_V(image, nullptr);
@ -390,70 +429,6 @@ GDMonoClass *GDMonoAssembly::get_class(MonoClass *p_mono_class) {
return wrapped_class;
}
GDMonoClass *GDMonoAssembly::get_object_derived_class(const StringName &p_class) {
GDMonoClass *match = nullptr;
if (gdobject_class_cache_updated) {
Map<StringName, GDMonoClass *>::Element *result = gdobject_class_cache.find(p_class);
if (result) {
match = result->get();
}
} else {
List<GDMonoClass *> nested_classes;
int rows = mono_image_get_table_rows(image, MONO_TABLE_TYPEDEF);
for (int i = 1; i < rows; i++) {
MonoClass *mono_class = mono_class_get(image, (i + 1) | MONO_TOKEN_TYPE_DEF);
if (!mono_class_is_assignable_from(CACHED_CLASS_RAW(GodotObject), mono_class)) {
continue;
}
GDMonoClass *current = get_class(mono_class);
if (!current) {
continue;
}
nested_classes.push_back(current);
if (!match && current->get_name() == p_class) {
match = current;
}
while (!nested_classes.is_empty()) {
GDMonoClass *current_nested = nested_classes.front()->get();
nested_classes.pop_front();
void *iter = nullptr;
while (true) {
MonoClass *raw_nested = mono_class_get_nested_types(current_nested->get_mono_ptr(), &iter);
if (!raw_nested) {
break;
}
GDMonoClass *nested_class = get_class(raw_nested);
if (nested_class) {
gdobject_class_cache.insert(nested_class->get_name(), nested_class);
nested_classes.push_back(nested_class);
}
}
}
gdobject_class_cache.insert(current->get_name(), current);
}
gdobject_class_cache_updated = true;
}
return match;
}
GDMonoAssembly *GDMonoAssembly::load(const String &p_name, MonoAssemblyName *p_aname, bool p_refonly, const Vector<String> &p_search_dirs) {
if (GDMono::get_singleton()->get_corlib_assembly() && (p_name == "mscorlib" || p_name == "mscorlib.dll")) {
return GDMono::get_singleton()->get_corlib_assembly();

View file

@ -71,13 +71,13 @@ class GDMonoAssembly {
MonoImage *image;
MonoAssembly *assembly;
bool attrs_fetched = false;
MonoCustomAttrInfo *attributes = nullptr;
#ifdef GD_MONO_HOT_RELOAD
uint64_t modified_time = 0;
#endif
bool gdobject_class_cache_updated = false;
Map<StringName, GDMonoClass *> gdobject_class_cache;
HashMap<ClassKey, GDMonoClass *, ClassKey::Hasher> cached_classes;
Map<MonoClass *, GDMonoClass *> cached_raw;
@ -111,11 +111,14 @@ public:
String get_path() const;
bool has_attribute(GDMonoClass *p_attr_class);
MonoObject *get_attribute(GDMonoClass *p_attr_class);
void fetch_attributes();
GDMonoClass *get_class(const StringName &p_namespace, const StringName &p_name);
GDMonoClass *get_class(MonoClass *p_mono_class);
GDMonoClass *get_object_derived_class(const StringName &p_class);
static String find_assembly(const String &p_name);
static void fill_search_dirs(Vector<String> &r_search_dirs, const String &p_custom_config = String(), const String &p_custom_bcl_dir = String());

View file

@ -148,6 +148,11 @@ void CachedData::clear_godot_api_cache() {
class_PuppetSyncAttribute = nullptr;
class_GodotMethodAttribute = nullptr;
field_GodotMethodAttribute_methodName = nullptr;
class_ScriptPathAttribute = nullptr;
field_ScriptPathAttribute_path = nullptr;
class_AssemblyHasScriptsAttribute = nullptr;
field_AssemblyHasScriptsAttribute_requiresLookup = nullptr;
field_AssemblyHasScriptsAttribute_scriptTypes = nullptr;
field_GodotObject_ptr = nullptr;
field_StringName_ptr = nullptr;
@ -272,6 +277,11 @@ void update_godot_api_cache() {
CACHE_CLASS_AND_CHECK(PuppetSyncAttribute, GODOT_API_CLASS(PuppetSyncAttribute));
CACHE_CLASS_AND_CHECK(GodotMethodAttribute, GODOT_API_CLASS(GodotMethodAttribute));
CACHE_FIELD_AND_CHECK(GodotMethodAttribute, methodName, CACHED_CLASS(GodotMethodAttribute)->get_field("methodName"));
CACHE_CLASS_AND_CHECK(ScriptPathAttribute, GODOT_API_CLASS(ScriptPathAttribute));
CACHE_FIELD_AND_CHECK(ScriptPathAttribute, path, CACHED_CLASS(ScriptPathAttribute)->get_field("path"));
CACHE_CLASS_AND_CHECK(AssemblyHasScriptsAttribute, GODOT_API_CLASS(AssemblyHasScriptsAttribute));
CACHE_FIELD_AND_CHECK(AssemblyHasScriptsAttribute, requiresLookup, CACHED_CLASS(AssemblyHasScriptsAttribute)->get_field("requiresLookup"));
CACHE_FIELD_AND_CHECK(AssemblyHasScriptsAttribute, scriptTypes, CACHED_CLASS(AssemblyHasScriptsAttribute)->get_field("scriptTypes"));
CACHE_FIELD_AND_CHECK(GodotObject, ptr, CACHED_CLASS(GodotObject)->get_field(BINDINGS_PTR_FIELD));
CACHE_FIELD_AND_CHECK(StringName, ptr, CACHED_CLASS(StringName)->get_field(BINDINGS_PTR_FIELD));

View file

@ -119,6 +119,11 @@ struct CachedData {
GDMonoClass *class_PuppetSyncAttribute;
GDMonoClass *class_GodotMethodAttribute;
GDMonoField *field_GodotMethodAttribute_methodName;
GDMonoClass *class_ScriptPathAttribute;
GDMonoField *field_ScriptPathAttribute_path;
GDMonoClass *class_AssemblyHasScriptsAttribute;
GDMonoField *field_AssemblyHasScriptsAttribute_requiresLookup;
GDMonoField *field_AssemblyHasScriptsAttribute_scriptTypes;
GDMonoField *field_GodotObject_ptr;
GDMonoField *field_StringName_ptr;