From 5b2b7e5bec5a6dda2146ee46c23f4b6c30d33273 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sat, 16 Mar 2024 15:07:57 +0900 Subject: [PATCH] feat: add support for declaring ProvidesParametersFor via base classes and interfaces Closes: #194 --- CHANGELOG.md | 1 + Editor/EnhancerDatabase.cs | 83 ++++++++++--- Runtime/TestSupport/PTCConflictComponent.cs | 14 +++ .../TestSupport/PTCConflictComponent.cs.meta | 3 + .../PTCDepthResolutionComponent.cs | 7 ++ .../PTCDepthResolutionComponent.cs.meta | 3 + .../PTCDepthResolutionComponentBase.cs | 11 ++ .../PTCDepthResolutionComponentBase.cs.meta | 3 + .../TestSupport/PTCInheritanceComponent.cs | 14 +++ .../PTCInheritanceComponent.cs.meta | 3 + .../InheritanceTests.cs | 111 ++++++++++++++++++ .../InheritanceTests.cs.meta | 3 + .../PluginResolverTests/BeforeAfterPlugin.cs | 1 - 13 files changed, 242 insertions(+), 15 deletions(-) create mode 100644 Runtime/TestSupport/PTCConflictComponent.cs create mode 100644 Runtime/TestSupport/PTCConflictComponent.cs.meta create mode 100644 Runtime/TestSupport/PTCDepthResolutionComponent.cs create mode 100644 Runtime/TestSupport/PTCDepthResolutionComponent.cs.meta create mode 100644 Runtime/TestSupport/PTCDepthResolutionComponentBase.cs create mode 100644 Runtime/TestSupport/PTCDepthResolutionComponentBase.cs.meta create mode 100644 Runtime/TestSupport/PTCInheritanceComponent.cs create mode 100644 Runtime/TestSupport/PTCInheritanceComponent.cs.meta create mode 100644 UnitTests~/ParameterIntrospection/InheritanceTests.cs create mode 100644 UnitTests~/ParameterIntrospection/InheritanceTests.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 18224a8..6081a75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] ### Added +- Added support for declaring ProvidesParametersFor via base classes and interfaces (#198) ### Fixed diff --git a/Editor/EnhancerDatabase.cs b/Editor/EnhancerDatabase.cs index a4cd1a9..d09b86b 100644 --- a/Editor/EnhancerDatabase.cs +++ b/Editor/EnhancerDatabase.cs @@ -1,7 +1,9 @@ #region using System; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; @@ -11,14 +13,23 @@ namespace nadena.dev.ndmf { - internal static class EnhancerDatabase where T : Attribute + internal class EnhancerDatabase where T : Attribute { internal delegate Interface Creator(Component c); private static readonly PropertyInfo forTypeProp = typeof(T).GetProperty("ForType"); - private static readonly Task> TaskDatabase = Task.Run(Init); + private static readonly Task> TaskDatabase = Task.Run(() => new EnhancerDatabase()); - private static IImmutableDictionary Init() + private readonly IImmutableDictionary _attributes = FindAttributes(); + private Dictionary _resolved; + + private EnhancerDatabase() + { + _resolved = new Dictionary.Creator>(); + } + + + private static IImmutableDictionary FindAttributes() { var builder = ImmutableDictionary.CreateBuilder(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) @@ -66,26 +77,70 @@ internal static class EnhancerDatabase where T : Attribute builder.Add(forType, lambda.Compile()); } - public static IImmutableDictionary Mappings + public static bool Query(Component c, out Interface iface) + { + iface = default; + if (!TaskDatabase.Result.DoQuery(c, out var creator)) return false; + + iface = creator(c); + + return true; + } + + private bool DoQuery(Component c, out Creator creator) { - get + if (_resolved.TryGetValue(c.GetType(), out creator)) { - TaskDatabase.Wait(); - return TaskDatabase.Result; + return true; } - } - public static bool Query(Component c, out Interface iface) - { - if (!Mappings.TryGetValue(c.GetType(), out var creator)) + var tmp = WalkTypeTree(c.GetType()) + .Where((kvp) => _attributes.ContainsKey(kvp.Item1)).ToList(); + + // Perform breadth-first search on base classes and interfaces, prioritizing more specific declarations. + using (var it = WalkTypeTree(c.GetType()) + .Where((kvp) => _attributes.ContainsKey(kvp.Item1)) + .OrderBy((kvp) => kvp.Item2) + .Take(2) + .GetEnumerator()) { - iface = default; - return false; + + if (!it.MoveNext()) return false; + var first = it.Current; + if (it.MoveNext() && it.Current.Item2 == first.Item2) + { + Debug.LogError("Multiple candidate " + typeof(T) + + " attributes found for base types and interfaces of " + c.GetType()); + return false; + } + + creator = _attributes[first.Item1]; + _resolved[c.GetType()] = creator; // cache resolved type } + - iface = creator(c); return true; } + + private IEnumerable<(Type, int)> WalkTypeTree(Type type) + { + int depth = 0; + while (type != null) + { + yield return (type, depth); + + foreach (var i in type.GetInterfaces()) + { + yield return (i, depth + 1); + } + + if (type.BaseType == type) break; + + type = type.BaseType; + depth++; + } + } + public static void AsyncInit() { diff --git a/Runtime/TestSupport/PTCConflictComponent.cs b/Runtime/TestSupport/PTCConflictComponent.cs new file mode 100644 index 0000000..4c67c13 --- /dev/null +++ b/Runtime/TestSupport/PTCConflictComponent.cs @@ -0,0 +1,14 @@ +using UnityEngine; + +namespace nadena.dev.ndmf.UnitTestSupport +{ + interface ITestInterface2 + { + } + + [AddComponentMenu("")] + internal class PTCConflictComponent : MonoBehaviour, ITestInterface1, ITestInterface2 + { + + } +} \ No newline at end of file diff --git a/Runtime/TestSupport/PTCConflictComponent.cs.meta b/Runtime/TestSupport/PTCConflictComponent.cs.meta new file mode 100644 index 0000000..d1c3da7 --- /dev/null +++ b/Runtime/TestSupport/PTCConflictComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f73e6824e7b74315a9d63f9767565e8a +timeCreated: 1710567106 \ No newline at end of file diff --git a/Runtime/TestSupport/PTCDepthResolutionComponent.cs b/Runtime/TestSupport/PTCDepthResolutionComponent.cs new file mode 100644 index 0000000..e1477e2 --- /dev/null +++ b/Runtime/TestSupport/PTCDepthResolutionComponent.cs @@ -0,0 +1,7 @@ +namespace nadena.dev.ndmf.UnitTestSupport +{ + internal class PTCDepthResolutionComponent : PTCDepthResolutionComponentBase, ITestInterface2 + { + + } +} \ No newline at end of file diff --git a/Runtime/TestSupport/PTCDepthResolutionComponent.cs.meta b/Runtime/TestSupport/PTCDepthResolutionComponent.cs.meta new file mode 100644 index 0000000..6816276 --- /dev/null +++ b/Runtime/TestSupport/PTCDepthResolutionComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8f840af0a8d2467ea17ec162b10b3a66 +timeCreated: 1710567132 \ No newline at end of file diff --git a/Runtime/TestSupport/PTCDepthResolutionComponentBase.cs b/Runtime/TestSupport/PTCDepthResolutionComponentBase.cs new file mode 100644 index 0000000..f437fc9 --- /dev/null +++ b/Runtime/TestSupport/PTCDepthResolutionComponentBase.cs @@ -0,0 +1,11 @@ +using UnityEngine; + +namespace nadena.dev.ndmf.UnitTestSupport +{ + internal abstract class PTCDepthResolutionComponentBase : PTCDepthResolutionComponentBase2 + { + } + + internal abstract class PTCDepthResolutionComponentBase2 : MonoBehaviour + { } +} \ No newline at end of file diff --git a/Runtime/TestSupport/PTCDepthResolutionComponentBase.cs.meta b/Runtime/TestSupport/PTCDepthResolutionComponentBase.cs.meta new file mode 100644 index 0000000..f4f8a1a --- /dev/null +++ b/Runtime/TestSupport/PTCDepthResolutionComponentBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 15f2d9c9e839460086ecb751e48c0b0d +timeCreated: 1710567184 \ No newline at end of file diff --git a/Runtime/TestSupport/PTCInheritanceComponent.cs b/Runtime/TestSupport/PTCInheritanceComponent.cs new file mode 100644 index 0000000..5cc5aa5 --- /dev/null +++ b/Runtime/TestSupport/PTCInheritanceComponent.cs @@ -0,0 +1,14 @@ +using UnityEngine; + +namespace nadena.dev.ndmf.UnitTestSupport +{ + interface ITestInterface1 + { + } + + [AddComponentMenu("")] + internal class PTCInheritanceComponent : MonoBehaviour, ITestInterface1 + { + + } +} \ No newline at end of file diff --git a/Runtime/TestSupport/PTCInheritanceComponent.cs.meta b/Runtime/TestSupport/PTCInheritanceComponent.cs.meta new file mode 100644 index 0000000..71ad1a6 --- /dev/null +++ b/Runtime/TestSupport/PTCInheritanceComponent.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 60665b6e4a3d4d1ba5b0f05a3a0ff04b +timeCreated: 1710567075 \ No newline at end of file diff --git a/UnitTests~/ParameterIntrospection/InheritanceTests.cs b/UnitTests~/ParameterIntrospection/InheritanceTests.cs new file mode 100644 index 0000000..95d29f8 --- /dev/null +++ b/UnitTests~/ParameterIntrospection/InheritanceTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.RegularExpressions; +using nadena.dev.ndmf; +using nadena.dev.ndmf.UnitTestSupport; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +#if NDMF_VRCSDK3_AVATARS + +namespace UnitTests.Parameters +{ + [ParameterProviderFor(typeof(ITestInterface1))] + internal class TestInterface1Provider : IParameterProvider + { + public TestInterface1Provider(ITestInterface1 _) + { + + } + + public IEnumerable GetSuppliedParameters(BuildContext context) + { + return Array.Empty(); + } + + public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context) + { + } + } + + [ParameterProviderFor(typeof(ITestInterface2))] + internal class TestInterface2Provider : IParameterProvider + { + public TestInterface2Provider(ITestInterface2 _) + { + + } + + public IEnumerable GetSuppliedParameters(BuildContext context) + { + return Array.Empty(); + } + + public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context) + { + } + } + + [ParameterProviderFor(typeof(PTCDepthResolutionComponentBase2))] + internal class DepthResolutionProvider : IParameterProvider + { + public DepthResolutionProvider(PTCDepthResolutionComponentBase2 _) + { + + } + + public IEnumerable GetSuppliedParameters(BuildContext context) + { + return Array.Empty(); + } + + public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context) + { + } + } + + public class InheritanceTest : TestBase + { + [Test] + public void ResolvesInterface() + { + var root = CreateRoot("root"); + var obj = root.AddComponent(); + + Assert.IsTrue(EnhancerDatabase.Query( + obj, out var provider + )); + + Assert.IsTrue(provider is TestInterface1Provider); + } + + [Test] + public void DoesNotResolveAmbiguous() + { + var root = CreateRoot("root"); + var obj = root.AddComponent(); + + Assert.IsFalse(EnhancerDatabase.Query( + obj, out var provider + )); + LogAssert.Expect(LogType.Error, new Regex("Multiple candidate .*ParameterProviderFor attributes")); + } + + [Test] + public void ResolvesByDepth() + { + var root = CreateRoot("root"); + var obj = root.AddComponent(); + + Assert.IsTrue(EnhancerDatabase.Query( + obj, out var provider + )); + + Assert.IsTrue(provider is TestInterface2Provider); + } + } +} + +#endif \ No newline at end of file diff --git a/UnitTests~/ParameterIntrospection/InheritanceTests.cs.meta b/UnitTests~/ParameterIntrospection/InheritanceTests.cs.meta new file mode 100644 index 0000000..174eccb --- /dev/null +++ b/UnitTests~/ParameterIntrospection/InheritanceTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 80f4faa0222d4a3ebbe44b9cd89e1b86 +timeCreated: 1710566997 \ No newline at end of file diff --git a/UnitTests~/PluginResolverTests/BeforeAfterPlugin.cs b/UnitTests~/PluginResolverTests/BeforeAfterPlugin.cs index 409fb3c..5935217 100644 --- a/UnitTests~/PluginResolverTests/BeforeAfterPlugin.cs +++ b/UnitTests~/PluginResolverTests/BeforeAfterPlugin.cs @@ -2,7 +2,6 @@ using System.Linq; using nadena.dev.ndmf; using NUnit.Framework; -using PlasticPipe.PlasticProtocol.Messages; namespace UnitTests.PluginResolverTests {