From c62248de09726742a5c646a32ee24f3e595700e8 Mon Sep 17 00:00:00 2001 From: Ashton Meuser Date: Sun, 17 May 2026 10:04:30 -0700 Subject: [PATCH 1/2] AllowNil property attribute --- src/Lua.Annotations/Attributes.cs | 2 + .../DiagnosticDescriptors.cs | 9 +++ .../LuaObjectGenerator.Emit.cs | 32 +++++++++- src/Lua.SourceGenerator/PropertyMetadata.cs | 10 ++++ tests/Lua.Tests/LuaObjectTests.cs | 59 ++++++++++++++++++- 5 files changed, 109 insertions(+), 3 deletions(-) diff --git a/src/Lua.Annotations/Attributes.cs b/src/Lua.Annotations/Attributes.cs index 46d68939..34026290 100644 --- a/src/Lua.Annotations/Attributes.cs +++ b/src/Lua.Annotations/Attributes.cs @@ -24,6 +24,8 @@ public LuaMemberAttribute(string name) } public string? Name { get; } + + public bool AllowNil { get; set; } } [AttributeUsage(AttributeTargets.Method)] diff --git a/src/Lua.SourceGenerator/DiagnosticDescriptors.cs b/src/Lua.SourceGenerator/DiagnosticDescriptors.cs index 0b05c66e..cc4d8b88 100644 --- a/src/Lua.SourceGenerator/DiagnosticDescriptors.cs +++ b/src/Lua.SourceGenerator/DiagnosticDescriptors.cs @@ -68,4 +68,13 @@ public static class DiagnosticDescriptors defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true ); + + public static readonly DiagnosticDescriptor InvalidAllowNilMemberType = new( + id: "LUACS008", + title: "AllowNil can only be used on LuaObject reference type members.", + messageFormat: "AllowNil can only be used on LuaObject reference type members.", + category: Category, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true + ); } diff --git a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs index 02f39a1f..00ef5145 100644 --- a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs +++ b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs @@ -32,6 +32,21 @@ Compilation compilation return $"{GetLuaValuePrefix(typeSymbol, references, compilation)}{expression})"; } + static bool CanAllowNil(ITypeSymbol typeSymbol, SymbolReferences references) + { + return typeSymbol.IsReferenceType && typeSymbol.ContainsAttribute(references.LuaObjectAttribute); + } + + static string GetContextArgumentExpression( + int argumentIndex, + ITypeSymbol typeSymbol, + bool allowNil + ) + { + var methodName = allowNil ? "GetArgumentOrDefault" : "GetArgument"; + return $"context.{methodName}<{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>({argumentIndex})"; + } + static bool TryEmit( TypeMetadata typeMetadata, CodeBuilder builder, @@ -229,6 +244,19 @@ Dictionary metaDict foreach (var property in typeMetadata.Properties) { + if (property.AllowNil && !CanAllowNil(property.Type, references)) + { + context.ReportDiagnostic( + Diagnostic.Create( + DiagnosticDescriptors.InvalidAllowNilMemberType, + property.Symbol.Locations.FirstOrDefault() + ) + ); + + isValid = false; + continue; + } + if (SymbolEqualityComparer.Default.Equals(property.Type, references.LuaValue)) { continue; @@ -667,7 +695,7 @@ TempCollections tempCollections else { builder.AppendLine( - $"{typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);" + $"{typeMetadata.FullTypeName}.{propertyMetadata.Symbol.Name} = {GetContextArgumentExpression(2, propertyMetadata.Type, propertyMetadata.AllowNil)};" ); } @@ -691,7 +719,7 @@ TempCollections tempCollections else { builder.AppendLine( - $"userData.{propertyMetadata.Symbol.Name} = context.GetArgument<{propertyMetadata.TypeFullName}>(2);" + $"userData.{propertyMetadata.Symbol.Name} = {GetContextArgumentExpression(2, propertyMetadata.Type, propertyMetadata.AllowNil)};" ); } diff --git a/src/Lua.SourceGenerator/PropertyMetadata.cs b/src/Lua.SourceGenerator/PropertyMetadata.cs index 0b152bfd..11dbb4ca 100644 --- a/src/Lua.SourceGenerator/PropertyMetadata.cs +++ b/src/Lua.SourceGenerator/PropertyMetadata.cs @@ -10,6 +10,7 @@ public class PropertyMetadata public bool IsStatic { get; } public bool IsReadOnly { get; } public bool IsWriteOnly { get; } + public bool AllowNil { get; } public string LuaMemberName { get; } public PropertyMetadata(ISymbol symbol, SymbolReferences references) @@ -51,6 +52,15 @@ public PropertyMetadata(ISymbol symbol, SymbolReferences references) LuaMemberName = str; } } + + foreach (var namedArgument in memberAttribute.NamedArguments) + { + if (namedArgument.Key == "AllowNil" && + namedArgument.Value.Value is bool allowNil) + { + AllowNil = allowNil; + } + } } } } diff --git a/tests/Lua.Tests/LuaObjectTests.cs b/tests/Lua.Tests/LuaObjectTests.cs index 5ea49fbf..7051e46b 100644 --- a/tests/Lua.Tests/LuaObjectTests.cs +++ b/tests/Lua.Tests/LuaObjectTests.cs @@ -137,6 +137,23 @@ public string Call() } } +[LuaObject] +public partial class AllowNilReferencedObject +{ + [LuaMember("label")] + public string Label { get; set; } = ""; +} + +[LuaObject] +public partial class AllowNilMemberContainer +{ + [LuaMember("optionalObject", AllowNil = true)] + public AllowNilReferencedObject OptionalObject { get; set; } = null!; + + [LuaMember("requiredObject")] + public AllowNilReferencedObject RequiredObject { get; set; } = null!; +} + [LuaObject] public partial class IntArrayUserData { @@ -194,7 +211,6 @@ public void SetAt(string key, int value) } } -[LuaObject] public abstract partial class ParentClass { [LuaMember("value")] @@ -239,6 +255,47 @@ public async Task Test_PropertyWithName() Assert.That(results[0], Is.EqualTo(new LuaValue("foo"))); } + [Test] + public async Task Test_LuaMemberAllowNilLuaObjectPropertyCanBeSetToNil() + { + var referencedObject = new AllowNilReferencedObject { Label = "reference" }; + var userData = new AllowNilMemberContainer { OptionalObject = referencedObject }; + + var state = LuaState.Create(); + state.Environment["referencedObject"] = referencedObject; + state.Environment["target"] = userData; + var results = await state.DoStringAsync(""" + local oldObject = target.optionalObject + target.optionalObject = nil + local nilObject = target.optionalObject + target.optionalObject = referencedObject + return oldObject.label, nilObject, target.optionalObject.label + """); + + Assert.That(results, Has.Length.EqualTo(3)); + Assert.That(results[0], Is.EqualTo(new LuaValue("reference"))); + Assert.That(results[1], Is.EqualTo(LuaValue.Nil)); + Assert.That(results[2], Is.EqualTo(new LuaValue("reference"))); + Assert.That(userData.OptionalObject, Is.SameAs(referencedObject)); + } + + [Test] + public async Task Test_LuaObjectPropertyRejectsNilWithoutAllowNil() + { + var referencedObject = new AllowNilReferencedObject { Label = "reference" }; + var userData = new AllowNilMemberContainer { RequiredObject = referencedObject }; + + var state = LuaState.Create(); + state.Environment["target"] = userData; + var exception = Assert.ThrowsAsync(async () => + { + await state.DoStringAsync("target.requiredObject = nil"); + }); + + Assert.That(exception!.Message, Does.Contain("bad argument #3")); + Assert.That(userData.RequiredObject, Is.SameAs(referencedObject)); + } + [Test] public async Task Test_MethodVoid() { From 17836fafd0393002bb05eedb7a5f06aa909f5f56 Mon Sep 17 00:00:00 2001 From: Ashton Meuser Date: Sun, 17 May 2026 11:54:25 -0700 Subject: [PATCH 2/2] Fix GetArgumentOrDefault reference --- src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs index 00ef5145..611f6e29 100644 --- a/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs +++ b/src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs @@ -43,8 +43,11 @@ static string GetContextArgumentExpression( bool allowNil ) { - var methodName = allowNil ? "GetArgumentOrDefault" : "GetArgument"; - return $"context.{methodName}<{typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>({argumentIndex})"; + var typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var argumentExpression = $"context.GetArgument<{typeName}>({argumentIndex})"; + return allowNil + ? $"context.HasArgument({argumentIndex}) ? {argumentExpression} : default({typeName})" + : argumentExpression; } static bool TryEmit(