Skip to content
3 changes: 3 additions & 0 deletions core/io/resource_format_binary.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@ Error ResourceLoaderBinary::load() {
res->set_edited(false);
#endif

res->notification(Object::NOTIFICATION_EXPORT_ASSIGNED);

if (progress) {
*progress = (i + 1) / float(internal_resources.size());
}
Expand Down Expand Up @@ -1252,6 +1254,7 @@ Ref<Resource> ResourceFormatLoaderBinary::load(const String &p_path, const Strin
if (err) {
return Ref<Resource>();
}

return loader.resource;
}

Expand Down
2 changes: 2 additions & 0 deletions core/object/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,8 @@ class Object {
NOTIFICATION_EXTENSION_RELOADED = 2,
// Internal notification to send after NOTIFICATION_PREDELETE, not bound to scripting.
NOTIFICATION_PREDELETE_CLEANUP = 3,
// Notification sent when all of the exported variables have been assigned after resource load or scene instantiation.
NOTIFICATION_EXPORT_ASSIGNED = 4,
};

/* TYPE API */
Expand Down
5 changes: 5 additions & 0 deletions modules/mono/csharp_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,11 @@ void CSharpInstance::notification(int p_notification, bool p_reversed) {
gchandle.get_intptr(), /* okIfNull */ false);

return;
} else if (p_notification == Object::NOTIFICATION_EXPORT_ASSIGNED) {
// When NOTIFICATION_EXPORT_ASSIGNED is sent, we take the chance to validate exported properties.
// This notification is sent after all the exported properties of a Resource or PackedScene instance
// have been assigned, which makes it a good point to all the validation logic.
GDMonoCache::managed_callbacks.CSharpInstanceBridge_ValidateExports(gchandle.get_intptr());
}

_call_notification(p_notification, p_reversed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Godot.SourceGenerators
{
Expand All @@ -16,6 +17,12 @@ public static bool TryGetGlobalAnalyzerProperty(
) => context.AnalyzerConfigOptions.GlobalOptions
.TryGetValue("build_property." + property, out value);

public static bool TryGetGlobalAnalyzerProperty(
this SuppressionAnalysisContext context, string property, out string? value
) => context.Options.AnalyzerConfigOptionsProvider.GlobalOptions
.TryGetValue("build_property." + property, out value);


public static bool AreGodotSourceGeneratorsDisabled(this GeneratorExecutionContext context)
=> context.TryGetGlobalAnalyzerProperty("GodotSourceGenerators", out string? toggle) &&
toggle != null &&
Expand All @@ -32,6 +39,16 @@ public static bool IsGodotSourceGeneratorDisabled(this GeneratorExecutionContext
disabledGenerators != null &&
disabledGenerators.Split(';').Contains(generatorName));

public static bool IsGodotEnableExportNullChecks(this GeneratorExecutionContext context)
=> context.TryGetGlobalAnalyzerProperty("GodotEnableExportNullChecks", out string? toggle) &&
toggle != null &&
toggle.Equals("true", StringComparison.OrdinalIgnoreCase);

public static bool IsGodotEnableExportNullChecks(this SuppressionAnalysisContext context)
=> context.TryGetGlobalAnalyzerProperty("GodotEnableExportNullChecks", out string? toggle) &&
toggle != null &&
toggle.Equals("true", StringComparison.OrdinalIgnoreCase);

public static bool InheritsFrom(this ITypeSymbol? symbol, string assemblyName, string typeFullName)
{
while (symbol != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<CompilerVisibleProperty Include="GodotProjectDirBase64" />
<CompilerVisibleProperty Include="GodotSourceGenerators" />
<CompilerVisibleProperty Include="IsGodotToolsProject" />
<CompilerVisibleProperty Include="GodotEnableExportNullChecks" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,74 @@ INamedTypeSymbol GetTypeByMetadataNameOrThrow(string fullyQualifiedMetadataName)
}
}

/// <summary>
/// Checks if the given type symbol is a StringName type from GodotSharp.
/// </summary>
public static bool IsStringNameType(ITypeSymbol typeSymbol)
{
return typeSymbol is INamedTypeSymbol namedType &&
namedType.ContainingAssembly?.Name == "GodotSharp" &&
namedType.ContainingNamespace?.Name == "Godot" &&
namedType.Name == "StringName";
}

/// <summary>
/// Checks if the given type symbol is a NodePath type from GodotSharp.
/// </summary>
public static bool IsNodePathType(ITypeSymbol typeSymbol)
{
return typeSymbol is INamedTypeSymbol namedType &&
namedType.ContainingAssembly?.Name == "GodotSharp" &&
namedType.ContainingNamespace?.Name == "Godot" &&
namedType.Name == "NodePath";
}

/// <summary>
/// Checks if the given type symbol is a packed array (C# array of Godot-compatible primitive or struct types).
/// </summary>
public static bool IsPackedArrayType(ITypeSymbol typeSymbol)
{
if (typeSymbol.TypeKind != TypeKind.Array)
return false;

var arrayType = (IArrayTypeSymbol)typeSymbol;
if (arrayType.Rank != 1)
return false;

var elementType = arrayType.ElementType;

// Check for primitive packed arrays
switch (elementType.SpecialType)
{
case SpecialType.System_Byte:
case SpecialType.System_Int32:
case SpecialType.System_Int64:
case SpecialType.System_Single:
case SpecialType.System_Double:
case SpecialType.System_String:
return true;
}

// Check for Godot struct packed arrays
if (elementType.ContainingAssembly?.Name == "GodotSharp" &&
elementType.ContainingNamespace?.Name == "Godot")
{
switch (elementType.Name)
{
case "Vector2":
case "Vector3":
case "Vector4":
case "Color":
case "StringName":
case "NodePath":
case "Rid":
return true;
}
}

return false;
}

public static VariantType? ConvertMarshalTypeToVariantType(MarshalType marshalType)
=> marshalType switch
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace Godot.SourceGenerators
{
internal static class NullableUtils
{
internal static bool IsNullableContextEnabledForSymbol(ISymbol symbol, Func<SyntaxTree, SemanticModel> semanticModelProvider)
{
// Get the syntax reference for the symbol declaration
var syntaxReference = symbol.DeclaringSyntaxReferences.FirstOrDefault();
if (syntaxReference == null)
return false;

var syntaxTree = syntaxReference.SyntaxTree;
var syntaxNode = syntaxReference.GetSyntax();

// Get the nullable context options at the declaration location
var semanticModel = semanticModelProvider(syntaxTree);
var nullableContext = semanticModel.GetNullableContext(syntaxNode.SpanStart);

// Check if nullable reference types are enabled (either as warnings or errors)
return (nullableContext & NullableContext.Enabled) != 0;
}

internal static bool IsExportedNonNullableGodotType(ISymbol memberSymbol, ITypeSymbol memberType, Func<SyntaxTree, SemanticModel> semanticModelProvider, bool requireNullableContext = true)
{
// Check if the member has the [Export] attribute
bool isExported = memberSymbol.GetAttributes()
.Any(a => a.AttributeClass?.IsGodotExportAttribute() ?? false);

if (!isExported)
return false;

// Check if it's a reference type and check nullable annotation
if (!memberType.IsReferenceType)
return false;

// Check if member has nullable context enabled (either via #nullable or project settings)
// This check can be skipped for the suppressor when we just need to identify exported Godot types
if (requireNullableContext && !IsNullableContextEnabledForSymbol(memberSymbol, semanticModelProvider))
return false;

// If the type is nullable annotated (e.g., Node?), skip it
if (memberType.NullableAnnotation == NullableAnnotation.Annotated)
return false;

// Check for string type
if (memberType.SpecialType == SpecialType.System_String)
return true;

// Check for packed arrays (System arrays of Godot-compatible types)
if (MarshalUtils.IsPackedArrayType(memberType))
return true;

// Check if the type is a Godot compatible class type (Node, Resource, or derived)
// This check must come after array check since arrays are not INamedTypeSymbol
if (memberType is not INamedTypeSymbol namedType)
return false;

// Check if the type inherits from Node or Resource
bool isNodeOrResource = namedType.InheritsFrom("GodotSharp", GodotClasses.Node) ||
namedType.InheritsFrom("GodotSharp", "Godot.Resource");

if (isNodeOrResource)
return true;

// Check if the type is Godot.Collections.Array or Dictionary (including generic variations)
string fullTypeName = namedType.ConstructedFrom.ToString();
bool isGodotCollection = fullTypeName == "Godot.Collections.Array" ||
fullTypeName == "Godot.Collections.Array<T>" ||
fullTypeName == "Godot.Collections.Dictionary" ||
fullTypeName == "Godot.Collections.Dictionary<TKey, TValue>";

if (isGodotCollection)
return true;

// Check for StringName and NodePath
if (MarshalUtils.IsStringNameType(namedType) || MarshalUtils.IsNodePathType(namedType))
return true;

return false;
}
}
}
Loading