diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b8516f..1baf3e95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## Version 1.1.0 + +### Changes + +- ID-qualified injection sites now require a matching `ToID(...)` binding. Unqualified bindings only match injection sites without an ID. +- If an `[Inject("id")]` site previously relied on an unqualified binding, add `.ToID("id")` to the binding or remove the ID from the injection site. + +### Fixes + +- Fixed `ToTarget` matching for inherited and nested targets. `.ToTarget()` now matches derived target types, `.ToTarget()` also applies to inherited injection members on the same derived target, and nested serializable injection targets are matched by their nested owner type. +- Fixed binding validation for duplicate or ambiguous qualifier combinations that could resolve the same injection site. + ## Version 1.0.5 ### Fixes diff --git a/Docs/docs/core-concepts/binding.md b/Docs/docs/core-concepts/binding.md index c5ef35cc..02396fc0 100644 --- a/Docs/docs/core-concepts/binding.md +++ b/Docs/docs/core-concepts/binding.md @@ -40,8 +40,8 @@ Most bindings follow this general pattern or a combination of these: ```csharp BindComponent() .ToID("hud") // Optional qualifier that matches [Inject("hud")] - .ToTarget() // Optional qualifier that matches injection targets of type CombatHud - .ToMember("audioService") // Optional qualifier that matches and methods named "audioService" + .ToTarget() // Optional qualifier that matches injection target objects of type CombatHud + .ToMember("audioService") // Optional qualifier that matches members named "audioService" .FromTargetSelf() // Required locator strategy that looks for the AudioManager on the transform of the CombatHud .WhereComponent(c => c.isActiveAndEnabled); // Optional filter that only includes enabled components ``` @@ -167,16 +167,18 @@ Full runtime proxy API: Binding qualifiers restrict which injection sites (fields, properties, methods) can be resolved from a binding. -| Qualifier | Injection site match | -|------------------------------|---------------------------------------------------------------| -| `ToID("someId")` | Fields, properties, methods marked with `[Inject("someId")]` | -| `ToTarget()` | Fields, properties, methods declared in a `TTarget` component | -| `ToMember("someMemberName")` | Fields, properties, methods with name `"someMemberName"` | +| Qualifier | Injection site match | +|------------------------------|---------------------------------------------------------------------------------| +| `ToID("someId")` | Fields, properties, methods marked with `[Inject("someId")]` | +| `ToTarget()` | Fields, properties, methods owned by `TTarget` objects and derived types | +| `ToMember("someMemberName")` | Fields, properties, methods with name `"someMemberName"` | Important behavior: - Binding qualifiers are additive, so all specified qualifiers must match. -- If a binding qualifier is not set on the binding, the binding matches by `TInterface` or `TConcrete` only. +- Injection sites without an ID match bindings without `ToID`. Injection sites with an ID only match bindings with the same `ToID`. +- If `ToTarget` or `ToMember` is not set on the binding, that qualifier does not restrict where the binding applies. +- `ToTarget()` matches the actual object that owns the injected member. This can be a component or a nested serializable object, and a binding targeted to a base type also matches derived types. - Binding qualifiers apply to component, asset, and runtime proxy bindings. - Binding qualifiers do not apply to global bindings. @@ -223,20 +225,20 @@ protected override void DeclareBindings() } ``` -| Binding Family | Filter Support | -|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| Component bindings | Yes | -| Asset bindings | Yes | -| Global bindings | Yes (same filter API as component bindings). | -| Runtime proxy bindings | No | +| Binding Family | Filter Support | +|------------------------|----------------------------------------------| +| Component bindings | Yes | +| Asset bindings | Yes | +| Global bindings | Yes (same filter API as component bindings). | +| Runtime proxy bindings | No | If a binding filter throws an exception, Saneject logs a binding filter error for that binding. ## Binding uniqueness -Saneject enforces binding unique within each `Scope`. When a binding is considered duplicate, Saneject logs an error and excludes the duplicate from the injection run. +Saneject enforces unambiguous bindings within each `Scope`. When two bindings are considered duplicate or ambiguous, Saneject logs an error and excludes the conflicting binding from the injection run. -Duplicate checks use these criteria: +Duplicate and ambiguity checks use these criteria: 1. Same scope. 2. Same binding family (`ComponentBindingNode`, `AssetBindingNode`, or `GlobalComponentBindingNode`). @@ -244,14 +246,17 @@ Duplicate checks use these criteria: - `TInterface` when present. - Otherwise `TConcrete`. 4. Same single/collection shape. -5. Qualifier overlap: - - If both bindings have no binding qualifiers at all, they conflict. - - Otherwise, conflict requires full overlap in `ToTarget`, `ToMember`, and `ToID` simultaneously. +5. Qualifier ambiguity: + - If the criteria above do not separate two bindings, they conflict unless at least one qualifier rule below separates them. + - `ToID` separates bindings by ID. A binding without `ToID` does not overlap a binding with `ToID`, and two bindings with `ToID` overlap only when their IDs overlap. + - `ToTarget` qualifiers overlap when their target type hierarchies overlap. + - `ToMember` qualifiers separate bindings only when both bindings specify non-overlapping values. + - Empty `ToTarget` and `ToMember` qualifier sets are unrestricted and do not separate bindings. Examples: ```csharp -// Duplicate: same scope, same family, same type, same shape, no qualifiers. +// Duplicate or ambiguous: same scope, same family, same type, same shape, no qualifiers. BindComponent() .FromScopeSelf(); @@ -274,6 +279,28 @@ BindAsset() .FromResources("Configs/Gameplay"); ``` +```csharp +// Distinct: one binding matches ID "menu" and the other matches injection sites without an ID. +BindAsset() + .ToID("menu") + .FromResources("Configs/Menu"); + +BindAsset() + .ToTarget() + .FromResources("Configs/Menu"); +``` + +```csharp +// Ambiguous: both bindings can resolve the config member on MainMenuController. +BindAsset() + .ToMember("config") + .FromResources("Configs/Menu"); + +BindAsset() + .ToTarget() + .FromResources("Configs/Menu"); +``` + Global bindings have an extra rule: only one global binding per concrete component type is allowed across active bindings across all scopes. A second `BindGlobal()` is invalid even if declared in another scope. diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/BindingValidator.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/BindingValidator.cs index d4a0ede7..91931927 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/BindingValidator.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/BindingValidator.cs @@ -43,7 +43,7 @@ private static void ValidateBinding( errors.Add(new InvalidBindingError ( bindingNode: bindingNode, - reason: "Duplicate binding within same Scope detected" + reason: "Duplicate or ambiguous binding within same Scope detected" )); switch (bindingNode) diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/Resolver.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/Resolver.cs index 65d8fd75..af0f3e85 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/Resolver.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Core/Resolver.cs @@ -18,9 +18,23 @@ public static void Resolve( InjectionContext context, InjectionProgressTracker progressTracker) { - ResolveGlobals(context, progressTracker); - ResolveFields(context, progressTracker); - ResolveMethods(context, progressTracker); + ResolveGlobals + ( + context, + progressTracker + ); + + ResolveFields + ( + context, + progressTracker + ); + + ResolveMethods + ( + context, + progressTracker + ); } private static void ResolveGlobals( @@ -49,16 +63,28 @@ out HashSet rejectedTypes ); if (candidates is not { Length: > 0 }) - context.RegisterError(new MissingGlobalDependencyError + context.RegisterError ( - binding, - rejectedTypes - )); + new MissingGlobalDependencyError + ( + binding, + rejectedTypes + ) + ); - if (!globalMap.TryGetValue(binding.ScopeNode, out HashSet dependencySet)) + if (!globalMap.TryGetValue + ( + binding.ScopeNode, + out HashSet dependencySet + )) { dependencySet = new HashSet(); - globalMap.Add(binding.ScopeNode, dependencySet); + + globalMap.Add + ( + binding.ScopeNode, + dependencySet + ); } object resolved = candidates.FirstOrDefault(); @@ -68,7 +94,11 @@ out HashSet rejectedTypes } foreach ((ScopeNode scopeNode, HashSet dependencies) in globalMap) - context.RegisterGlobalDependencies(scopeNode, dependencies); + context.RegisterGlobalDependencies + ( + scopeNode, + dependencies + ); } private static void ResolveFields( @@ -85,7 +115,13 @@ private static void ResolveFields( foreach (FieldNode fieldNode in fieldNodes) { progressTracker.UpdateInfoText($"Resolving field: {fieldNode.ShortPath}"); - ResolveField(fieldNode, context); + + ResolveField + ( + fieldNode, + context + ); + progressTracker.NextStep(); } } @@ -104,7 +140,13 @@ private static void ResolveMethods( foreach (MethodNode methodNode in methodNodes) { progressTracker.UpdateInfoText($"Resolving method: {methodNode.ShortPath}"); - ResolveMethod(methodNode, context); + + ResolveMethod + ( + methodNode, + context + ); + progressTracker.NextStep(); } } @@ -117,7 +159,7 @@ private static void ResolveField( ( currentScope: fieldNode.ComponentNode.TransformNode.NearestScopeNode, context: context, - declaringType: fieldNode.DeclaringType, + targetType: fieldNode.Owner.GetType(), requestedType: fieldNode.RequestedType, isCollection: fieldNode.IsCollection, qualifyingMemberName: fieldNode.QualifyingName, @@ -144,12 +186,15 @@ out HashSet rejectedTypes ); if (candidates is not { Length: > 0 }) - context.RegisterError(new MissingDependencyError + context.RegisterError ( - bindingNode, - fieldNode, - rejectedTypes - )); + new MissingDependencyError + ( + bindingNode, + fieldNode, + rejectedTypes + ) + ); resolved = ResolveCandidates ( @@ -162,7 +207,11 @@ out HashSet rejectedTypes context.RegisterUsedBinding(bindingNode); } - context.RegisterFieldDependency(fieldNode, resolved); + context.RegisterFieldDependency + ( + fieldNode, + resolved + ); } private static void ResolveMethod( @@ -177,7 +226,7 @@ private static void ResolveMethod( ( currentScope: methodNode.ComponentNode.TransformNode.NearestScopeNode, context: context, - declaringType: methodNode.DeclaringType, + targetType: methodNode.Owner.GetType(), requestedType: parameterNode.RequestedType, isCollection: parameterNode.IsCollection, qualifyingMemberName: methodNode.QualifyingName, @@ -204,12 +253,15 @@ out HashSet rejectedTypes ); if (candidates is not { Length: > 0 }) - context.RegisterError(new MissingDependencyError + context.RegisterError ( - bindingNode, - parameterNode, - rejectedTypes - )); + new MissingDependencyError + ( + bindingNode, + parameterNode, + rejectedTypes + ) + ); resolved = ResolveCandidates ( @@ -224,13 +276,17 @@ out HashSet rejectedTypes context.RegisterUsedBinding(bindingNode); } - context.RegisterMethodDependencies(methodNode, resolvedParameters); + context.RegisterMethodDependencies + ( + methodNode, + resolvedParameters + ); } private static BindingNode FindMatchingBindingNode( ScopeNode currentScope, InjectionContext context, - Type declaringType, + Type targetType, Type requestedType, bool isCollection, string qualifyingMemberName, @@ -277,7 +333,7 @@ bool MatchesIsCollection(BindingNode bindingNode) bool MatchesTargetTypeQualifiers(BindingNode bindingNode) { return bindingNode.TargetTypeQualifiers.Count == 0 || - bindingNode.TargetTypeQualifiers.Contains(declaringType); + bindingNode.TargetTypeQualifiers.Any(t => t.IsAssignableFrom(targetType)); } bool MatchesMemberNameQualifiers(BindingNode bindingNode) @@ -288,8 +344,10 @@ bool MatchesMemberNameQualifiers(BindingNode bindingNode) bool MatchesIdQualifiers(BindingNode bindingNode) { - return bindingNode.IdQualifiers.Count == 0 || - bindingNode.IdQualifiers.Contains(injectId); + if (!string.IsNullOrWhiteSpace(injectId)) + return bindingNode.IdQualifiers.Count > 0 && bindingNode.IdQualifiers.Contains(injectId); + + return bindingNode.IdQualifiers.Count == 0; } } @@ -311,10 +369,18 @@ private static object ResolveCandidates( case TypeShape.Array: { - Array array = Array.CreateInstance(requestedType, candidates.Length); + Array array = Array.CreateInstance + ( + requestedType, + candidates.Length + ); for (int i = 0; i < candidates.Length; i++) - array.SetValue(candidates[i], i); + array.SetValue + ( + candidates[i], + i + ); return array; } @@ -334,4 +400,4 @@ private static object ResolveCandidates( } } } -} \ No newline at end of file +} diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/BindingNode.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/BindingNode.cs index 32e6bb1b..ca29f4e3 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/BindingNode.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/BindingNode.cs @@ -47,32 +47,47 @@ public bool Equals(BindingNode other) if (other is null) return false; - if (ReferenceEquals(this, other)) + if (ReferenceEquals + ( + this, + other + )) return true; - if (!Equals(ScopeNode, other.ScopeNode)) + if (!Equals + ( + ScopeNode, + other.ScopeNode + )) return false; if (other.GetType() != GetType()) return false; - if (InterfaceType != null ? InterfaceType != other.InterfaceType : ConcreteType != other.ConcreteType) + if (InterfaceType != null + ? InterfaceType != other.InterfaceType + : ConcreteType != other.ConcreteType) return false; if (IsCollectionBinding != other.IsCollectionBinding) return false; - if (TargetTypeQualifiers.Count == 0 - && MemberNameQualifiers.Count == 0 - && IdQualifiers.Count == 0 - && other.TargetTypeQualifiers.Count == 0 - && other.MemberNameQualifiers.Count == 0 - && other.IdQualifiers.Count == 0) - return true; + bool separatedByTarget = + TargetTypeQualifiers.Count > 0 && + other.TargetTypeQualifiers.Count > 0 && + !TargetTypeQualifiers.OverlapsWith(other.TargetTypeQualifiers); + + bool separatedByMemberName = + MemberNameQualifiers.Count > 0 && + other.MemberNameQualifiers.Count > 0 && + !MemberNameQualifiers.OverlapsWith(other.MemberNameQualifiers); + + bool separatedById = (IdQualifiers.Count > 0 || other.IdQualifiers.Count > 0) && + !IdQualifiers.OverlapsWith(other.IdQualifiers); - return TargetTypeQualifiers.OverlapsWith(other.TargetTypeQualifiers) - && MemberNameQualifiers.OverlapsWith(other.MemberNameQualifiers) - && IdQualifiers.OverlapsWith(other.IdQualifiers); + return !separatedByTarget + && !separatedByMemberName + && !separatedById; } public override bool Equals(object obj) @@ -87,10 +102,7 @@ public override int GetHashCode() ScopeNode, GetType(), InterfaceType ?? ConcreteType, - IsCollectionBinding, - TargetTypeQualifiers.Count > 0, - MemberNameQualifiers.Count > 0, - IdQualifiers.Count > 0 + IsCollectionBinding ); } } diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/MemberNode.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/MemberNode.cs index 91b636c4..54e45616 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/MemberNode.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Data/Graph/Nodes/MemberNode.cs @@ -19,7 +19,6 @@ protected MemberNode( { Owner = owner; ComponentNode = componentNode; - DeclaringType = memberInfo.DeclaringType; QualifyingName = NameUtility.GetLogicalName(memberInfo.Name); InjectId = injectAttribute.ID; SuppressMissingErrors = injectAttribute.SuppressMissingErrors; @@ -29,7 +28,6 @@ protected MemberNode( public object Owner { get; } public ComponentNode ComponentNode { get; } - public Type DeclaringType { get; } public string QualifyingName { get; } public string InjectId { get; } public bool SuppressMissingErrors { get; } diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Extensions/EnumerableOverlapExtensions.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Extensions/EnumerableOverlapExtensions.cs index 324c4489..825cb8e9 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Extensions/EnumerableOverlapExtensions.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Editor/Extensions/EnumerableOverlapExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; + // ReSharper disable ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator namespace Plugins.Saneject.Editor.Extensions @@ -28,8 +29,17 @@ public static bool OverlapsWith( this IEnumerable a, IEnumerable b) { - HashSet aSet = new(a.Where(s => !string.IsNullOrWhiteSpace(s)), StringComparer.Ordinal); - HashSet bSet = new(b.Where(s => !string.IsNullOrWhiteSpace(s)), StringComparer.Ordinal); + HashSet aSet = new + ( + a.Where(s => !string.IsNullOrWhiteSpace(s)), + StringComparer.Ordinal + ); + + HashSet bSet = new + ( + b.Where(s => !string.IsNullOrWhiteSpace(s)), + StringComparer.Ordinal + ); foreach (string id in aSet) if (bSet.Contains(id)) diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Attributes/InjectAttribute.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Attributes/InjectAttribute.cs index 57d2beb2..371eca10 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Attributes/InjectAttribute.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Attributes/InjectAttribute.cs @@ -16,10 +16,10 @@ namespace Plugins.Saneject.Runtime.Attributes public sealed class InjectAttribute : PropertyAttribute { /// - /// Marks the field or method for injection, using only type-based binding resolution. + /// Marks the field or method for injection without an ID. /// /// - /// The dependency is resolved by matching the member's type against bindings in the scope hierarchy. + /// The dependency is resolved by matching the member's type against bindings in the scope hierarchy that do not specify an ID. /// public InjectAttribute() { @@ -49,7 +49,7 @@ public InjectAttribute(bool suppressMissingErrors) /// /// Marks the field or method for injection with an ID while optionally suppressing missing binding and missing dependency logs for the field. /// - /// The binding ID to match against. + /// The binding ID to match against. Only bindings with the same ID will be used for resolution. /// /// If true, suppresses error logs when no binding or dependency is found. /// @@ -62,7 +62,7 @@ public InjectAttribute( } /// - /// Gets the binding ID used for dependency resolution, or null if only type-based resolution is used. + /// Gets the binding ID used for dependency resolution, or null if bindings without an ID should be used. /// public string ID { get; } diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Asset/AssetBindingBuilder.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Asset/AssetBindingBuilder.cs index e827676f..34c7d3db 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Asset/AssetBindingBuilder.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Asset/AssetBindingBuilder.cs @@ -63,11 +63,10 @@ public AssetFilterBuilder FromFolder(string folderPath) #region QUALIFIER METHODS /// - /// Qualifies this binding with an ID. - /// Only injection targets annotated with + /// Only injection sites annotated with /// that specify the same ID will resolve using this binding. /// - /// The identifiers to match against injection targets. + /// The identifiers to match against injection sites. /// The builder instance for fluent chaining. public AssetBindingBuilder ToID(params string[] ids) { @@ -76,8 +75,8 @@ public AssetBindingBuilder ToID(params string[] ids) } /// - /// Qualifies this binding to apply only when the injection target is of the given type. - /// The injection target is the that owns the field or property + /// Qualifies this binding to apply only when the injection target is of the given type or a derived type. + /// The injection target is the object that owns the field, property, or method /// marked with . /// /// The target type this binding applies to. @@ -89,8 +88,8 @@ public AssetBindingBuilder ToTarget() } /// - /// Qualifies this binding to apply only when the injection target is one of the specified types. - /// The injection target is the that owns the field or property + /// Qualifies this binding to apply only when the injection target is one of the specified types or a derived type. + /// The injection target is the object that owns the field, property, or method /// marked with . /// /// One or more target objects to match against. @@ -102,10 +101,10 @@ public AssetBindingBuilder ToTarget(params Type[] targetTypes) } /// - /// Qualifies this binding to apply only when the injection target member (field or property) + /// Qualifies this binding to apply only when the injection target member (field, property, or method) /// has one of the specified names. /// - /// The field or property names on the injection target that this binding should apply to. + /// The field, property, or method names on the injection target that this binding should apply to. /// The builder instance for fluent chaining. public AssetBindingBuilder ToMember(params string[] memberNames) { @@ -206,4 +205,4 @@ public AssetFilterBuilder FromMethod(Func> method) #endregion } -} \ No newline at end of file +} diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Component/ComponentBindingBuilder.cs b/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Component/ComponentBindingBuilder.cs index 3cecce92..a1078520 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Component/ComponentBindingBuilder.cs +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/Runtime/Bindings/Component/ComponentBindingBuilder.cs @@ -49,8 +49,7 @@ public RuntimeProxyBindingBuilder FromRuntimeProxy() #region QUALIFIER METHODS /// - /// Qualifies this binding with one or more IDs. - /// Only injection sites (fields/properties/methods) annotated with an + /// Only injection sites annotated with /// that specify the same ID will resolve using this binding. /// /// The identifiers to match against injection sites. @@ -62,8 +61,8 @@ public ComponentBindingBuilder ToID(params string[] ids) } /// - /// Qualifies this binding to apply only when the injection target is of the given type. - /// The injection target is the that owns the field, property or method + /// Qualifies this binding to apply only when the injection target is of the given type or a derived type. + /// The injection target is the object that owns the field, property or method /// marked with . /// /// The target type this binding applies to. @@ -75,8 +74,8 @@ public ComponentBindingBuilder ToTarget() } /// - /// Qualifies this binding to apply only when the injection target is one of the specified types. - /// The injection target is the that owns the field, property or method + /// Qualifies this binding to apply only when the injection target is one of the specified types or a derived type. + /// The injection target is the object that owns the field, property or method /// marked with . /// /// One or more target objects to match against. @@ -88,9 +87,9 @@ public ComponentBindingBuilder ToTarget(params Type[] targetTypes) } /// - /// Qualifies this binding to apply only when the injection target member (field or property) has one of the specified names. + /// Qualifies this binding to apply only when the injection target member (field, property, or method) has one of the specified names. /// - /// The field or property names on the injection target that this binding should apply to. + /// The field, property, or method names on the injection target that this binding should apply to. /// The builder instance for fluent chaining. public ComponentBindingBuilder ToMember(params string[] memberNames) { diff --git a/UnityProject/Saneject/Assets/Plugins/Saneject/package.json b/UnityProject/Saneject/Assets/Plugins/Saneject/package.json index a789229b..40e07e04 100644 --- a/UnityProject/Saneject/Assets/Plugins/Saneject/package.json +++ b/UnityProject/Saneject/Assets/Plugins/Saneject/package.json @@ -2,7 +2,7 @@ "name": "com.alexanderlarsen.saneject", "author": "Alexander Larsen", "displayName": "Saneject", - "version": "1.0.5", + "version": "1.1.0", "description": "Inject dependencies in the Unity Editor, not Play Mode, by writing them directly into serialized fields at edit-time using familiar DI APIs, so everything stays visible in the Inspector, including interfaces.\n\nNo runtime container. No startup cost. No hidden wiring. No weird lifecycles. Just simple, deterministic edit-time DI that works with Unity, not around it.", "documentationUrl": "https://github.com/alexanderlarsen/Saneject/blob/main/README.md", "changelogUrl": "https://github.com/alexanderlarsen/Saneject/blob/main/CHANGELOG.md", @@ -16,4 +16,4 @@ "path": "Samples~/DemoGame" } ] -} +} \ No newline at end of file diff --git a/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Equality/BindingNodeEqualityTests.cs b/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Equality/BindingNodeEqualityTests.cs index 077d1863..1a354b4f 100644 --- a/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Equality/BindingNodeEqualityTests.cs +++ b/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Equality/BindingNodeEqualityTests.cs @@ -161,7 +161,7 @@ public void BindingNode_SameScope_TConcrete_WithOverlappingQualifiers_IsEqual() } [Test] - public void BindingNode_SameScope_TConcrete_WithOnlyIDQualifiers_IsNotEqual() + public void BindingNode_SameScope_TConcrete_WithOnlyIDQualifiers_IsEqual() { // Set up scene TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1); @@ -181,10 +181,138 @@ public void BindingNode_SameScope_TConcrete_WithOnlyIDQualifiers_IsNotEqual() BindingNode firstBinding = scopeNode.BindingNodes[0]; BindingNode secondBinding = scopeNode.BindingNodes[1]; + // Assert + Assert.That(firstBinding, Is.EqualTo(secondBinding)); + } + + [Test] + public void BindingNode_SameScope_TConcrete_WithOnlyOverlappingIDQualifiers_IsEqual() + { + // Set up scene + TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1); + TestScope scope = scene.Add("Root 1"); + + // Bind + scope.BindComponent() + .ToID("qualified", "alternate") + .FromSelf(); + + scope.BindComponent() + .ToID("alternate", "other") + .FromAnywhere(); + + // Build graph and fetch binding nodes + ScopeNode scopeNode = CreateScopeNode(scene, "Root 1"); + BindingNode firstBinding = scopeNode.BindingNodes[0]; + BindingNode secondBinding = scopeNode.BindingNodes[1]; + + // Assert + Assert.That(firstBinding, Is.EqualTo(secondBinding)); + } + + [Test] + public void BindingNode_SameScope_TConcrete_WithOnlyDifferentIDQualifiers_IsNotEqual() + { + // Set up scene + TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1); + TestScope scope = scene.Add("Root 1"); + + // Bind + scope.BindComponent() + .ToID("qualified", "alternate") + .FromSelf(); + + scope.BindComponent() + .ToID("other", "fallback") + .FromAnywhere(); + + // Build graph and fetch binding nodes + ScopeNode scopeNode = CreateScopeNode(scene, "Root 1"); + BindingNode firstBinding = scopeNode.BindingNodes[0]; + BindingNode secondBinding = scopeNode.BindingNodes[1]; + // Assert Assert.That(firstBinding, Is.Not.EqualTo(secondBinding)); } + [Test] + public void BindingNode_SameScope_TConcrete_WithIDAndTargetQualifiers_IsNotEqual() + { + // Set up scene + TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1); + TestScope scope = scene.Add("Root 1"); + + // Bind + scope.BindComponent() + .ToID("qualified") + .FromSelf(); + + scope.BindComponent() + .ToTarget() + .FromAnywhere(); + + // Build graph and fetch binding nodes + ScopeNode scopeNode = CreateScopeNode(scene, "Root 1"); + BindingNode firstBinding = scopeNode.BindingNodes[0]; + BindingNode secondBinding = scopeNode.BindingNodes[1]; + + // Assert + Assert.That(firstBinding, Is.Not.EqualTo(secondBinding)); + } + + [Test] + public void BindingNode_SameScope_TConcrete_WithIDAndOverlappingTargetAndMemberQualifiers_IsNotEqual() + { + // Set up scene + TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1); + TestScope scope = scene.Add("Root 1"); + + // Bind + scope.BindComponent() + .ToTarget() + .ToMember("dependency") + .FromSelf(); + + scope.BindComponent() + .ToID("qualified") + .ToTarget() + .ToMember("dependency") + .FromAnywhere(); + + // Build graph and fetch binding nodes + ScopeNode scopeNode = CreateScopeNode(scene, "Root 1"); + BindingNode firstBinding = scopeNode.BindingNodes[0]; + BindingNode secondBinding = scopeNode.BindingNodes[1]; + + // Assert + Assert.That(firstBinding, Is.Not.EqualTo(secondBinding)); + } + + [Test] + public void BindingNode_SameScope_TConcrete_WithOverlappingTargetAndMemberQualifiers_IsEqual() + { + // Set up scene + TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1); + TestScope scope = scene.Add("Root 1"); + + // Bind + scope.BindComponent() + .ToTarget() + .FromSelf(); + + scope.BindComponent() + .ToMember("dependency") + .FromAnywhere(); + + // Build graph and fetch binding nodes + ScopeNode scopeNode = CreateScopeNode(scene, "Root 1"); + BindingNode firstBinding = scopeNode.BindingNodes[0]; + BindingNode secondBinding = scopeNode.BindingNodes[1]; + + // Assert + Assert.That(firstBinding, Is.EqualTo(secondBinding)); + } + [Test] public void BindingNode_SameScope_TConcrete_WithAssignableTargetQualifiers_IsEqual() { @@ -311,4 +439,4 @@ private static ScopeNode CreateScopeNode( return scopeNode; } } -} \ No newline at end of file +} diff --git a/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Qualification/ComponentBindingQualifierTests.cs b/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Qualification/ComponentBindingQualifierTests.cs index 3615568f..dc75ea38 100644 --- a/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Qualification/ComponentBindingQualifierTests.cs +++ b/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Binding/Qualification/ComponentBindingQualifierTests.cs @@ -131,5 +131,60 @@ public void ToTarget_Generic_TConcrete_InjectsMatchingTarget_NotDifferentTarget( Assert.That(matchingTarget.dependency, Is.EqualTo(dependency)); Assert.That(nonMatchingTarget.dependency, Is.Null); } + + [Test] + public void ToTarget_Generic_TConcrete_InjectsNestedSerializableTarget() + { + // Set up scene + TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 2); + TestScope scope = scene.Add("Root 1"); + NestedRootTarget target = scene.Add("Root 1"); + + // Find dependency + AssetDependency assetDependency = Resources.Load("AssetDependency 1"); + ComponentDependency singleDependency = scene.Add("Root 1"); + + ComponentDependency[] dependencies = + { + singleDependency, + scene.Add("Root 1/Child 1") + }; + + // Bind + scope.BindAsset() + .ToTarget() + .FromAssetLoad("Assets/Tests/Saneject/Fixtures/Resources/AssetDependency 1.asset"); + + scope.BindAsset() + .ToID("nested-method-id") + .ToTarget() + .FromAssetLoad("Assets/Tests/Saneject/Fixtures/Resources/AssetDependency 1.asset"); + + scope.BindComponents() + .ToID("nested-method-id") + .ToTarget() + .FromDescendants(includeSelf: true); + + scope.BindComponent() + .ToID("nested-method-id") + .ToTarget() + .FromSelf(); + + // Inject + InjectionRunner.Run(scene.Roots, ContextWalkFilter.SceneObjects); + + // Assert + Assert.That(assetDependency, Is.Not.Null); + Assert.That(target.nested.nestedFieldDependency, Is.EqualTo(assetDependency)); + Assert.That(target.nested.NestedMethodAssetDependency, Is.EqualTo(assetDependency)); + + CollectionAssert.AreEquivalent + ( + dependencies, + target.nested.NestedMethodComponentDependencies + ); + + Assert.That(target.nested.NestedMethodInterfaceDependency, Is.EqualTo(singleDependency)); + } } -} \ No newline at end of file +} diff --git a/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Graph/InjectionGraphMemberTests.cs b/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Graph/InjectionGraphMemberTests.cs index 6983ee86..f8410c8a 100644 --- a/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Graph/InjectionGraphMemberTests.cs +++ b/UnityProject/Saneject/Assets/Tests/Saneject/Editor/Graph/InjectionGraphMemberTests.cs @@ -47,7 +47,6 @@ public void FieldNode_GivenTopLevelFieldAndPropertyTargets_CapturesFieldMetadata FieldNode concreteFieldNode = concreteFieldComponentNode.FieldNodes.Single(); Assert.That(concreteFieldNode.Owner, Is.EqualTo(concreteFieldComponentNode.Component)); - Assert.That(concreteFieldNode.DeclaringType, Is.EqualTo(typeof(SingleConcreteComponentTarget))); Assert.That(concreteFieldNode.QualifyingName, Is.EqualTo("dependency")); Assert.That(concreteFieldNode.InjectId, Is.Null); Assert.That(concreteFieldNode.SuppressMissingErrors, Is.False); @@ -61,7 +60,6 @@ public void FieldNode_GivenTopLevelFieldAndPropertyTargets_CapturesFieldMetadata Assert.That(concreteFieldNode.ShortPath, Is.EqualTo("SingleConcreteComponentTarget.dependency")); FieldNode interfaceFieldNode = interfaceFieldComponentNode.FieldNodes.Single(); - Assert.That(interfaceFieldNode.DeclaringType, Is.EqualTo(typeof(SingleInterfaceTarget))); Assert.That(interfaceFieldNode.QualifyingName, Is.EqualTo("dependency")); Assert.That(interfaceFieldNode.FieldType, Is.EqualTo(typeof(IDependency))); Assert.That(interfaceFieldNode.RequestedType, Is.EqualTo(typeof(IDependency))); @@ -73,7 +71,6 @@ public void FieldNode_GivenTopLevelFieldAndPropertyTargets_CapturesFieldMetadata Assert.That(interfaceFieldNode.ShortPath, Is.EqualTo("SingleInterfaceTarget.dependency")); FieldNode concretePropertyNode = concretePropertyComponentNode.FieldNodes.Single(); - Assert.That(concretePropertyNode.DeclaringType, Is.EqualTo(typeof(SingleConcreteComponentPropertyTarget))); Assert.That(concretePropertyNode.QualifyingName, Is.EqualTo("Dependency")); Assert.That(concretePropertyNode.FieldType, Is.EqualTo(typeof(ComponentDependency))); Assert.That(concretePropertyNode.RequestedType, Is.EqualTo(typeof(ComponentDependency))); @@ -163,7 +160,6 @@ public void MethodNode_GivenMethodTargets_CapturesMethodAndParameterMetadata() MethodNode singleMethodNode = singleMethodComponentNode.MethodNodes.Single(); Assert.That(singleMethodNode.Owner, Is.EqualTo(singleMethodComponentNode.Component)); - Assert.That(singleMethodNode.DeclaringType, Is.EqualTo(typeof(SingleConcreteComponentMethodTarget))); Assert.That(singleMethodNode.QualifyingName, Is.EqualTo("Inject")); Assert.That(singleMethodNode.InjectId, Is.Null); Assert.That(singleMethodNode.SuppressMissingErrors, Is.False); @@ -282,14 +278,12 @@ public void ComponentNode_GivenNestedSerializableMembers_CapturesNestedOwnerPath Assert.That(componentNode.MethodNodes.Count, Is.EqualTo(2)); Assert.That(topLevelFieldNode.Owner, Is.EqualTo(target)); - Assert.That(topLevelFieldNode.DeclaringType, Is.EqualTo(typeof(NestedRootTarget))); Assert.That(topLevelFieldNode.InjectId, Is.EqualTo("field-id")); Assert.That(topLevelFieldNode.SuppressMissingErrors, Is.True); Assert.That(topLevelFieldNode.DisplayPath, Is.EqualTo("Root 1/NestedRootTarget/fieldDependency")); Assert.That(topLevelFieldNode.ShortPath, Is.EqualTo("NestedRootTarget.fieldDependency")); Assert.That(topLevelPropertyNode.Owner, Is.EqualTo(target)); - Assert.That(topLevelPropertyNode.DeclaringType, Is.EqualTo(typeof(NestedRootTarget))); Assert.That(topLevelPropertyNode.InjectId, Is.EqualTo("property-id")); Assert.That(topLevelPropertyNode.SuppressMissingErrors, Is.True); Assert.That(topLevelPropertyNode.TypeShape, Is.EqualTo(TypeShape.List)); @@ -299,14 +293,12 @@ public void ComponentNode_GivenNestedSerializableMembers_CapturesNestedOwnerPath Assert.That(topLevelPropertyNode.ShortPath, Is.EqualTo("NestedRootTarget.PropertyDependencies")); Assert.That(nestedFieldNode.Owner, Is.EqualTo(target.nested)); - Assert.That(nestedFieldNode.DeclaringType, Is.EqualTo(typeof(NestedChildTarget))); Assert.That(nestedFieldNode.InjectId, Is.Null); Assert.That(nestedFieldNode.SuppressMissingErrors, Is.False); Assert.That(nestedFieldNode.DisplayPath, Is.EqualTo("Root 1/NestedRootTarget/nested.nestedFieldDependency")); Assert.That(nestedFieldNode.ShortPath, Is.EqualTo("NestedChildTarget.nestedFieldDependency")); Assert.That(nestedPropertyNode.Owner, Is.EqualTo(target.nested)); - Assert.That(nestedPropertyNode.DeclaringType, Is.EqualTo(typeof(NestedChildTarget))); Assert.That(nestedPropertyNode.InjectId, Is.EqualTo("nested-property-id")); Assert.That(nestedPropertyNode.SuppressMissingErrors, Is.True); Assert.That(nestedPropertyNode.IsPropertyBackingField, Is.True); @@ -314,7 +306,6 @@ public void ComponentNode_GivenNestedSerializableMembers_CapturesNestedOwnerPath Assert.That(nestedPropertyNode.ShortPath, Is.EqualTo("NestedChildTarget.NestedPropertyDependency")); Assert.That(deepFieldNode.Owner, Is.EqualTo(target.nested.deepNested)); - Assert.That(deepFieldNode.DeclaringType, Is.EqualTo(typeof(NestedDeepTarget))); Assert.That(deepFieldNode.InjectId, Is.EqualTo("deep-field-id")); Assert.That(deepFieldNode.SuppressMissingErrors, Is.True); Assert.That(deepFieldNode.DisplayPath, Is.EqualTo("Root 1/NestedRootTarget/nested.deepNested.deepFieldDependency")); @@ -323,7 +314,6 @@ public void ComponentNode_GivenNestedSerializableMembers_CapturesNestedOwnerPath CollectionAssert.IsEmpty(componentNode.FieldNodes.Where(node => node.DisplayPath.Contains("nullNested")).ToArray()); Assert.That(topLevelMethodNode.Owner, Is.EqualTo(target)); - Assert.That(topLevelMethodNode.DeclaringType, Is.EqualTo(typeof(NestedRootTarget))); Assert.That(topLevelMethodNode.InjectId, Is.EqualTo("method-id")); Assert.That(topLevelMethodNode.SuppressMissingErrors, Is.True); Assert.That(topLevelMethodNode.DisplayPath, Is.EqualTo("Root 1/NestedRootTarget/InjectTopLevel")); @@ -342,7 +332,6 @@ public void ComponentNode_GivenNestedSerializableMembers_CapturesNestedOwnerPath ); Assert.That(nestedMethodNode.Owner, Is.EqualTo(target.nested)); - Assert.That(nestedMethodNode.DeclaringType, Is.EqualTo(typeof(NestedChildTarget))); Assert.That(nestedMethodNode.InjectId, Is.EqualTo("nested-method-id")); Assert.That(nestedMethodNode.SuppressMissingErrors, Is.False); Assert.That(nestedMethodNode.DisplayPath, Is.EqualTo("Root 1/NestedRootTarget/nested.InjectNested"));