From 1d6538bc5f8a9ec6e5d098059f4efc501df2edd9 Mon Sep 17 00:00:00 2001 From: akeit0 <90429982+Akeit0@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:24:02 +0900 Subject: [PATCH] Fix inherited LuaObject member and metamethod replacements --- src/Lua.SourceGenerator/MethodMetadata.cs | 4 +- src/Lua.SourceGenerator/TypeMetadata.cs | 106 +++++--- .../Utilities/SymbolExtensions.cs | 25 +- .../LuaObjectGeneratorTests.cs | 97 ++++++++ tests/Lua.Tests/LuaObjectInheritanceTests.cs | 233 ++++++++++++++++++ 5 files changed, 411 insertions(+), 54 deletions(-) create mode 100644 tests/Lua.Tests/LuaObjectInheritanceTests.cs diff --git a/src/Lua.SourceGenerator/MethodMetadata.cs b/src/Lua.SourceGenerator/MethodMetadata.cs index 10329d97..0f3396c9 100644 --- a/src/Lua.SourceGenerator/MethodMetadata.cs +++ b/src/Lua.SourceGenerator/MethodMetadata.cs @@ -8,8 +8,8 @@ class MethodMetadata public bool IsStatic { get; } public bool IsAsync { get; } public bool HasReturnValue { get; } - public bool HasMemberAttribute { get; } - public bool HasMetamethodAttribute { get; } + public bool HasMemberAttribute { get; set; } + public bool HasMetamethodAttribute { get; set; } public string LuaMemberName { get; } public LuaObjectMetamethod Metamethod { get; } diff --git a/src/Lua.SourceGenerator/TypeMetadata.cs b/src/Lua.SourceGenerator/TypeMetadata.cs index 6ed9cc67..d529a655 100644 --- a/src/Lua.SourceGenerator/TypeMetadata.cs +++ b/src/Lua.SourceGenerator/TypeMetadata.cs @@ -25,50 +25,92 @@ SymbolReferences references TypeName = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); FullTypeName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - Properties = Symbol - .GetAllMembers(false) - .Where(x => x is (IFieldSymbol or IPropertySymbol) and { IsImplicitlyDeclared: false }) - .Where(x => + var properties = new List(); + var methods = new List(); + var memberOwners = new Dictionary(); + var metamethodOwners = new Dictionary(); + + foreach (var member in Symbol.GetAllMembers()) + { + if ( + member.IsImplicitlyDeclared + || member.ContainsAttribute(references.LuaIgnoreMemberAttribute) + ) + { + continue; + } + + if (member is IFieldSymbol or IPropertySymbol) { - if (!x.ContainsAttribute(references.LuaMemberAttribute)) + if ( + !member.ContainsAttribute(references.LuaMemberAttribute) + || member is IPropertySymbol { IsIndexer: true } + ) { - return false; + continue; } - if (x.ContainsAttribute(references.LuaIgnoreMemberAttribute)) + var property = new PropertyMetadata(member, references); + if (SelectBinding(memberOwners, property.LuaMemberName, member)) { - return false; + properties.Add(property); } - - if (x is IPropertySymbol p) + } + else if (member is IMethodSymbol methodSymbol) + { + if ( + !member.ContainsAttribute(references.LuaMemberAttribute) + && !member.ContainsAttribute(references.LuaMetamethodAttribute) + ) { - if (p.IsIndexer) - { - return false; - } + continue; } - return true; - }) - .Select(x => new PropertyMetadata(x, references)) - .ToArray(); - - Methods = Symbol - .GetAllMembers(false) - .Where(x => x is IMethodSymbol and { IsImplicitlyDeclared: false }) - .Select(x => (IMethodSymbol)x) - .Where(x => - { - if (x.ContainsAttribute(references.LuaIgnoreMemberAttribute)) + var method = new MethodMetadata(methodSymbol, references); + // A method's member and metamethod bindings can be replaced independently. + if (method.HasMemberAttribute) + { + method.HasMemberAttribute = SelectBinding( + memberOwners, + method.LuaMemberName, + member + ); + } + if (method.HasMetamethodAttribute) + { + method.HasMetamethodAttribute = SelectBinding( + metamethodOwners, + method.Metamethod, + member + ); + } + if (method.HasMemberAttribute || method.HasMetamethodAttribute) { - return false; + methods.Add(method); } + } + } + + Properties = properties.ToArray(); + Methods = methods.ToArray(); + } + + static bool SelectBinding( + Dictionary owners, + TKey key, + ISymbol member + ) + where TKey : notnull + { + if (!owners.TryGetValue(key, out var owner)) + { + owners.Add(key, member.ContainingType); + return true; + } - return x.ContainsAttribute(references.LuaMemberAttribute) - || x.ContainsAttribute(references.LuaMetamethodAttribute); - }) - .Select(x => new MethodMetadata(x, references)) - .ToArray(); + // Derived bindings replace inherited ones. Keep duplicates within one type + // so that invalid declarations still produce diagnostics. + return SymbolEqualityComparer.Default.Equals(owner, member.ContainingType); } public bool IsPartial() diff --git a/src/Lua.SourceGenerator/Utilities/SymbolExtensions.cs b/src/Lua.SourceGenerator/Utilities/SymbolExtensions.cs index 555220d7..f8450c89 100644 --- a/src/Lua.SourceGenerator/Utilities/SymbolExtensions.cs +++ b/src/Lua.SourceGenerator/Utilities/SymbolExtensions.cs @@ -20,29 +20,14 @@ public static bool ContainsAttribute(this ISymbol symbol, INamedTypeSymbol attri ); } - public static IEnumerable GetAllMembers( - this INamedTypeSymbol symbol, - bool withoutOverride = true - ) + public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol) { - // Iterate Parent -> Derived - if (symbol.BaseType != null) + // Include overrides at every level, with the most derived declarations first. + for (var current = symbol; current != null; current = current.BaseType) { - foreach (var item in GetAllMembers(symbol.BaseType)) + foreach (var member in current.GetMembers()) { - // override item already iterated in parent type - if (!withoutOverride || !item.IsOverride) - { - yield return item; - } - } - } - - foreach (var item in symbol.GetMembers()) - { - if (!withoutOverride || !item.IsOverride) - { - yield return item; + yield return member; } } } diff --git a/tests/Lua.SourceGenerator.Tests/LuaObjectGeneratorTests.cs b/tests/Lua.SourceGenerator.Tests/LuaObjectGeneratorTests.cs index bee67c48..a69a4725 100644 --- a/tests/Lua.SourceGenerator.Tests/LuaObjectGeneratorTests.cs +++ b/tests/Lua.SourceGenerator.Tests/LuaObjectGeneratorTests.cs @@ -7,6 +7,103 @@ namespace Lua.SourceGenerator.Tests; public class LuaObjectGeneratorTests { + [TestCase( + "[LuaMetamethod(LuaObjectMetamethod.Call)] public static BaseObject New() => new();", + "[LuaMetamethod(LuaObjectMetamethod.Call)] public new static DerivedObject New() => new();" + )] + [TestCase( + "[LuaMember] public virtual int Value { get; set; }", + "[LuaMember] public override int Value { get; set; }" + )] + [TestCase("[LuaMember] public int Value;", "[LuaMember] public new int Value;")] + [TestCase( + "[LuaMember] public virtual int Read() => 1;", + "[LuaMember] public override int Read() => 2;" + )] + [TestCase( + "[LuaMember(\"read\")] public int BaseRead() => 1;", + "[LuaMember(\"read\")] public int DerivedRead() => 2;" + )] + [TestCase( + "[LuaMember(\"value\")] public int BaseValue { get; set; }", + "[LuaMember(\"value\")] public int DerivedValue;" + )] + [TestCase( + "[LuaMember(\"value\")] public int BaseValue { get; set; }", + "[LuaMember(\"value\")] public int DerivedRead() => 2;" + )] + [TestCase( + "[LuaMember(\"value\")] public int BaseRead() => 1;", + "[LuaMember(\"value\")] public int DerivedValue { get; set; }" + )] + [TestCase( + "[LuaMetamethod(LuaObjectMetamethod.Len)] public int BaseLength() => 1;", + "[LuaMetamethod(LuaObjectMetamethod.Len)] public int DerivedLength() => 2;" + )] + [TestCase("[LuaMember] public virtual int Read() => 1;", "public override int Read() => 2;")] + public void DerivedBindings_ReplaceInheritedBindings(string baseMember, string derivedMember) + { + var result = CompileGeneratedSource( + $$""" + using Lua; + [LuaObject] + internal partial class BaseObject { {{baseMember}} } + [LuaObject] + internal partial class DerivedObject : BaseObject { {{derivedMember}} } + [LuaObject] + internal partial class LeafObject : DerivedObject { } + """ + ); + + Assert.That(result.Success, Is.True, string.Join("\n", result.Diagnostics)); + Assert.That(result.Diagnostics, Is.Empty); + } + + [Test] + public void DuplicateMetamethods_InTheSameTypeRemainErrors() + { + var result = CompileGeneratedSource( + """ + using Lua; + [LuaObject] + internal partial class BaseObject + { + [LuaMetamethod(LuaObjectMetamethod.Len)] public int Length() => 1; + } + [LuaObject] + internal partial class DerivedObject : BaseObject + { + [LuaMetamethod(LuaObjectMetamethod.Len)] public int FirstLength() => 2; + [LuaMetamethod(LuaObjectMetamethod.Len)] public int SecondLength() => 3; + } + """ + ); + + Assert.That( + result.Diagnostics, + Has.Exactly(1).Matches(x => x.Id == "LUACS007") + ); + } + + [Test] + public void DuplicateLuaNames_InTheSameTypeRemainErrors() + { + var result = CompileGeneratedSource( + """ + using Lua; + [LuaObject] + internal partial class DuplicateObject + { + [LuaMember("read")] public int FirstRead() => 1; + [LuaMember("read")] public int SecondRead() => 2; + } + """ + ); + + Assert.That(result.Success, Is.False); + Assert.That(result.Diagnostics, Has.Some.Matches(x => x.Id == "CS0102")); + } + [TestCase("class DocumentedObject", false)] [TestCase("class DocumentedObject", true)] [TestCase("class DocumentedObject", false)] diff --git a/tests/Lua.Tests/LuaObjectInheritanceTests.cs b/tests/Lua.Tests/LuaObjectInheritanceTests.cs new file mode 100644 index 00000000..a60b0757 --- /dev/null +++ b/tests/Lua.Tests/LuaObjectInheritanceTests.cs @@ -0,0 +1,233 @@ +using Lua.Standard; + +namespace Lua.Tests; + +[LuaObject] +public partial class InheritanceBaseObject +{ + [LuaMember("value")] + public virtual int Value { get; set; } + + [LuaMember("field")] + public int Field; + + [LuaMember("read")] + public virtual string Read() => "base"; + + [LuaMember("inherited")] + public int Inherited() => 7; + + [LuaMember("dual"), LuaMetamethod(LuaObjectMetamethod.Len)] + public int Dual() => 1; + + [LuaMetamethod(LuaObjectMetamethod.Call)] + public static InheritanceBaseObject New() => new(); + + [LuaMetamethod(LuaObjectMetamethod.ToString)] + public override string ToString() => "base"; + + [LuaMetamethod(LuaObjectMetamethod.Index)] + public string Index(string key) => "base:" + key; + + [LuaMetamethod(LuaObjectMetamethod.NewIndex)] + public void NewIndex(string key, int value) => Field = value; +} + +[LuaObject] +public partial class InheritanceDerivedObject : InheritanceBaseObject +{ + [LuaMember("value")] + public override int Value { get; set; } + + [LuaMember("field")] + public new int Field; + + [LuaMember("read")] + public override string Read() => "derived"; + + [LuaMember("dual")] + public int MemberOnly() => 2; + + [LuaMetamethod(LuaObjectMetamethod.Call)] + public static new InheritanceDerivedObject New() => new(); + + [LuaMetamethod(LuaObjectMetamethod.ToString)] + public override string ToString() => "derived"; + + [LuaMetamethod(LuaObjectMetamethod.Index)] + public string DerivedIndex(string key) => "derived:" + key; + + [LuaMetamethod(LuaObjectMetamethod.NewIndex)] + public void DerivedNewIndex(string key, int value) => Field = value * 10; +} + +[LuaObject] +public partial class InheritanceLeafObject : InheritanceDerivedObject +{ + [LuaMetamethod(LuaObjectMetamethod.Len)] + public int Length() => 3; +} + +[LuaObject] +public partial class InheritanceMetamethodOnlyObject : InheritanceBaseObject +{ + [LuaMetamethod(LuaObjectMetamethod.Len)] + public int Length() => 4; +} + +[LuaObject] +public partial class InheritanceUnannotatedOverrideObject : InheritanceBaseObject +{ + public override string Read() => "unannotated"; + + public override string ToString() => "unannotated"; + + public static new InheritanceBaseObject New() => new InheritanceUnannotatedOverrideObject(); +} + +[LuaObject] +public partial class InheritanceAliasObject : InheritanceBaseObject +{ + [LuaMember("value")] + public int DifferentValue { get; set; } + + [LuaMember("read")] + public int DifferentRead() => 42; + + [LuaMember("inherited")] + public int MethodReplacedByProperty { get; set; } + + [LuaMember("field")] + public int PropertyReplacedByMethod() => 99; +} + +public class LuaObjectInheritanceTests +{ + [Test] + public async Task DerivedBindings_AreUsedWithoutChangingBaseBindings() + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + var parent = new InheritanceBaseObject(); + var child = new InheritanceDerivedObject(); + state.Environment["parent"] = parent; + state.Environment["child"] = child; + + var results = await state.DoStringAsync( + """ + parent.value = 10 + parent.field = 11 + child.value = 20 + child.field = 21 + child.unknown = 3 + return parent:read(), child:read(), parent.value, child.value, + parent.field, child.field, parent:inherited(), child:inherited(), + tostring(parent), tostring(child), parent.missing, child.missing, + parent(), child() + """ + ); + + Assert.That( + results.Take(12), + Is.EqualTo( + new LuaValue[] + { + "base", + "derived", + 10, + 20, + 11, + 30, + 7, + 7, + "base", + "derived", + "base:missing", + "derived:missing", + } + ) + ); + Assert.That(results[12].Read(), Is.TypeOf()); + Assert.That(results[13].Read(), Is.TypeOf()); + Assert.That(((InheritanceBaseObject)child).Field, Is.Zero); + } + + [Test] + public async Task LeafObjects_KeepIntermediateOverridesAndIndependentBindings() + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + state.Environment["parent"] = new InheritanceBaseObject(); + state.Environment["child"] = new InheritanceDerivedObject(); + state.Environment["leaf"] = new InheritanceLeafObject(); + state.Environment["metaOnly"] = new InheritanceMetamethodOnlyObject(); + + var results = await state.DoStringAsync( + """ + leaf.value = 12 + leaf.field = 13 + return parent:dual(), #parent, child:dual(), #child, leaf:dual(), #leaf, + leaf:read(), leaf.value, leaf.field, tostring(leaf), leaf.missing, + metaOnly:dual(), #metaOnly, leaf() + """ + ); + + Assert.That( + results.Take(13), + Is.EqualTo( + new LuaValue[] + { + 1, + 1, + 2, + 1, + 2, + 3, + "derived", + 12, + 13, + "derived", + "derived:missing", + 1, + 4, + } + ) + ); + Assert.That(results[13].Read(), Is.TypeOf()); + } + + [Test] + public async Task UnannotatedOverrides_KeepInheritedExposure() + { + using var state = LuaState.Create(); + state.OpenStandardLibraries(); + state.Environment["obj"] = new InheritanceUnannotatedOverrideObject(); + + var results = await state.DoStringAsync("return obj:read(), tostring(obj), obj()"); + + Assert.That(results[0], Is.EqualTo(new LuaValue("unannotated"))); + Assert.That(results[1], Is.EqualTo(new LuaValue("unannotated"))); + Assert.That(results[2].Read(), Is.TypeOf()); + } + + [Test] + public async Task LuaNames_SelectDerivedMembersAcrossMemberKinds() + { + using var state = LuaState.Create(); + var obj = new InheritanceAliasObject(); + state.Environment["obj"] = obj; + + var results = await state.DoStringAsync( + """ + obj.value = 12 + obj.inherited = 13 + return obj.value, obj:read(), obj.inherited, obj:field() + """ + ); + + Assert.That(results, Is.EqualTo(new LuaValue[] { 12, 42, 13, 99 })); + Assert.That(obj.DifferentValue, Is.EqualTo(12)); + Assert.That(obj.MethodReplacedByProperty, Is.EqualTo(13)); + Assert.That(obj.Value, Is.Zero); + } +}