Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Lua.Annotations/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public LuaMemberAttribute(string name)
}

public string? Name { get; }

public bool AllowNil { get; set; }
}

[AttributeUsage(AttributeTargets.Method)]
Expand Down
9 changes: 9 additions & 0 deletions src/Lua.SourceGenerator/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
35 changes: 33 additions & 2 deletions src/Lua.SourceGenerator/LuaObjectGenerator.Emit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ 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 typeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var argumentExpression = $"context.GetArgument<{typeName}>({argumentIndex})";
return allowNil
? $"context.HasArgument({argumentIndex}) ? {argumentExpression} : default({typeName})"
: argumentExpression;
}

static bool TryEmit(
TypeMetadata typeMetadata,
CodeBuilder builder,
Expand Down Expand Up @@ -229,6 +247,19 @@ Dictionary<INamedTypeSymbol, TypeMetadata> 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;
Expand Down Expand Up @@ -667,7 +698,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)};"
);
}

Expand All @@ -691,7 +722,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)};"
);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Lua.SourceGenerator/PropertyMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
}
}
}
}
59 changes: 58 additions & 1 deletion tests/Lua.Tests/LuaObjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -194,7 +211,6 @@ public void SetAt(string key, int value)
}
}

[LuaObject]
public abstract partial class ParentClass
{
[LuaMember("value")]
Expand Down Expand Up @@ -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<LuaRuntimeException>(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()
{
Expand Down