Skip to content

Commit

Permalink
Add Validation for disconnected dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
mikhail-dcl committed Oct 23, 2023
1 parent 06e66cc commit aac53ea
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 39 deletions.
18 changes: 15 additions & 3 deletions Arch.SystemGroups.SourceGenerator/CommonUtils.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
Expand Down Expand Up @@ -63,6 +64,17 @@ public static string GetTypeReferenceInGlobalNotation(ITypeSymbol typeSymbol)

if (IsGenericArgument(typeSymbol))
return typeSymbol.ToString();

// Append each generic argument within typeSymbol with "global::" prefix
if (typeSymbol is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: > 0 } namedTypeSymbol)
{
var genericArguments = GetGenericArguments(typeSymbol, true);

var originalDefinitionName = namedTypeSymbol.OriginalDefinition.ToString(); // Ends with '<TName,...TNameN>'
var genericStartIndex = originalDefinitionName.IndexOf('<');

return "global::" + originalDefinitionName.Remove(genericStartIndex) + genericArguments;
}

return "global::" + typeSymbol;
}
Expand All @@ -87,7 +99,7 @@ public static bool IsGenericArgument(ISymbol symbol)
public static bool IsGenericType(this ITypeSymbol typeSymbol) =>
typeSymbol is INamedTypeSymbol { IsGenericType: true, TypeArguments.Length: > 0 };

public static string GetGenericArguments(ITypeSymbol typeSymbol)
public static string GetGenericArguments(ITypeSymbol typeSymbol, bool globalNotation)
{
if (!(typeSymbol is INamedTypeSymbol { IsGenericType: true } namedTypeSymbol) || namedTypeSymbol.TypeArguments.Length == 0)
return string.Empty;
Expand All @@ -97,7 +109,7 @@ public static string GetGenericArguments(ITypeSymbol typeSymbol)
for (int i = 0; i < namedTypeSymbol.TypeArguments.Length; i++)
{
ITypeSymbol typeArgument = namedTypeSymbol.TypeArguments[i];
genericArgumentsBuilder.Append(typeArgument.Name);
genericArgumentsBuilder.Append(globalNotation ? GetTypeReferenceInGlobalNotation(typeArgument) : typeArgument.Name);

if (i < namedTypeSymbol.TypeArguments.Length - 1)
{
Expand Down
4 changes: 2 additions & 2 deletions Arch.SystemGroups.SourceGenerator/CreateGroupGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public static class CreateGroupGenerator
public static string GetTryGetCreateGroup(in GroupInfo groupInfo)
{
return groupInfo.Behaviour is { IsCustom: true, HasParameterlessConstructor: false }
? $"worldBuilder.TryRegisterGroup<{groupInfo.ClassName}>(typeof({groupInfo.UpdateInGroup}), {EdgesGenerator.AddEdgesCachedFieldName}, {groupInfo.ThrottlingEnabled.ToString().ToLower()});"
: $"worldBuilder.TryCreateGroup<{groupInfo.ClassName}>(typeof({groupInfo.UpdateInGroup}), {EdgesGenerator.AddEdgesCachedFieldName}, {groupInfo.ThrottlingEnabled.ToString().ToLower()});";
? $"worldBuilder.TryRegisterGroup<{groupInfo.ClassName}>(typeof({groupInfo.UpdateInGroup}), {EdgesGenerator.AddEdgesCachedFieldName}, {EdgesGenerator.ValidateEdgesCachedFieldName}, {groupInfo.ThrottlingEnabled.ToString().ToLower()});"
: $"worldBuilder.TryCreateGroup<{groupInfo.ClassName}>(typeof({groupInfo.UpdateInGroup}), {EdgesGenerator.AddEdgesCachedFieldName}, {EdgesGenerator.ValidateEdgesCachedFieldName}, {groupInfo.ThrottlingEnabled.ToString().ToLower()});";
}
}
31 changes: 31 additions & 0 deletions Arch.SystemGroups.SourceGenerator/EdgesGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

Expand All @@ -7,6 +8,36 @@ namespace Arch.SystemGroups.SourceGenerator
public static class EdgesGenerator
{
public const string AddEdgesCachedFieldName = "_addEdgesCached";
public const string ValidateEdgesCachedFieldName = "_validateEdgesCached";
public const string ValidateEdgesMethodName = "ValidateEdges";
public const string DisconnectedDependenciesFieldName = "disconnectedDependencies";

public static string GetValidateEdgesCachedField() =>
$"private static readonly Action<List<DisconnectedDependenciesInfo.WrongTypeBinding>> {ValidateEdgesCachedFieldName} = {ValidateEdgesMethodName};";

public static string GetValidateEdgesBody(IList<ITypeSymbol> updateBefore, IList<ITypeSymbol> updateAfter,
ITypeSymbol group, string className, ITypeSymbol thisType, string typeGenericArguments)
{
var builder = new StringBuilder();

foreach (var dependency in updateBefore)
InsertValidateEdge(dependency);

foreach (var dependency in updateAfter)
InsertValidateEdge(dependency);

return builder.ToString();

void InsertValidateEdge(ITypeSymbol dependency)
{
// Filter out references to self
if (dependency.Equals(thisType, SymbolEqualityComparer.Default))
return;

builder.AppendFormat(
$"ArchSystemsSorter.ValidateEdge({DisconnectedDependenciesFieldName}, typeof({className}{typeGenericArguments}), typeof({group}), typeof({dependency}), {dependency}.Metadata.UpdateInGroup);");
}
}

public static string GetAddEdgesCachedField() =>
$"private static readonly Action<Dictionary<Type, List<Type>>> {AddEdgesCachedFieldName} = AddEdges;";
Expand Down
2 changes: 2 additions & 0 deletions Arch.SystemGroups.SourceGenerator/InjectToWorldGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public static StringBuilder GetAddToGroup(in SystemInfo systemInfo, string typeG
sb.Append($"typeof({systemInfo.ClassName}{typeGenericArguments}), ");
sb.Append(EdgesGenerator.AddEdgesCachedFieldName);
sb.Append(", ");
sb.Append(EdgesGenerator.ValidateEdgesCachedFieldName);
sb.Append(", ");
sb.Append(systemInfo.ThrottlingEnabled.ToString().ToLower());
sb.Append(");");

Expand Down
24 changes: 23 additions & 1 deletion Arch.SystemGroups.SourceGenerator/PartialClassGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,15 @@ private static string AccessibilityToString(Accessibility accessibility)

private static string GetSystemPartialClass(in SystemInfo systemInfo, WorldInfo worldInfo)
{
var typeGenericArguments = CommonUtils.GetGenericArguments(systemInfo.This);
var typeGenericArguments = CommonUtils.GetGenericArguments(systemInfo.This, false);
var typeGenericArgumentsWhere = CommonUtils.GetGenericConstraintsString(systemInfo.This);
var addEdgesFieldDeclaration = EdgesGenerator.GetAddEdgesCachedField();
var addEdgesBody = EdgesGenerator.GetAddEdgesBody(systemInfo.UpdateBefore, systemInfo.UpdateAfter, systemInfo.ClassName, systemInfo.This, typeGenericArguments);

var validateEdgesFieldDeclaration = EdgesGenerator.GetValidateEdgesCachedField();
var validateEdgesBody = EdgesGenerator.GetValidateEdgesBody(systemInfo.UpdateBefore, systemInfo.UpdateAfter,
systemInfo.UpdateInGroup, systemInfo.ClassName, systemInfo.This, typeGenericArguments);

var injectToWorldMethodParams = InjectToWorldGenerator.GetMethodArgumentsWithoutWorld(in systemInfo).ToString();
var passArguments = InjectToWorldGenerator.GetPassArguments(in systemInfo).ToString();
var systemInstantiation = InjectToWorldGenerator.GetSystemInstantiation(in systemInfo, typeGenericArguments);
Expand Down Expand Up @@ -241,6 +245,8 @@ private static string GetSystemPartialClass(in SystemInfo systemInfo, WorldInfo
{{systemInfo.AccessModifier}} partial class {{systemInfo.ClassName}}{{typeGenericArguments}}{
{{addEdgesFieldDeclaration}}
{{validateEdgesFieldDeclaration}}
public static {{systemInfo.ClassName}}{{typeGenericArguments}} InjectToWorld(ref ArchSystemsWorldBuilder<{{worldType}}> worldBuilder{{injectToWorldMethodParams}})
{
Expand All @@ -254,6 +260,11 @@ private static void AddEdges(Dictionary<Type, List<Type>> edgesMap)
{
{{addEdgesBody}}
}
private static void {{EdgesGenerator.ValidateEdgesMethodName}}(List<DisconnectedDependenciesInfo.WrongTypeBinding> {{EdgesGenerator.DisconnectedDependenciesFieldName}})
{
{{validateEdgesBody}}
}
{{metadata}}
Expand All @@ -271,6 +282,10 @@ public static string GetGroupPartialClass(in GroupInfo groupInfo)
var addEdgesFieldDeclaration = EdgesGenerator.GetAddEdgesCachedField();
var addEdgesBody = EdgesGenerator.GetAddEdgesBody(groupInfo.UpdateBefore, groupInfo.UpdateAfter, groupInfo.ClassName, groupInfo.This, string.Empty);

var validateEdgesFieldDeclaration = EdgesGenerator.GetValidateEdgesCachedField();
var validateEdgesBody = EdgesGenerator.GetValidateEdgesBody(groupInfo.UpdateBefore, groupInfo.UpdateAfter,
groupInfo.UpdateInGroup, groupInfo.ClassName, groupInfo.This, string.Empty);

// If there is no system directly attached to the group we need to create the tree of groups, otherwise the leaf system will be detached
var groupInjection = InjectToWorldGenerator.GetGroupInjectionInvocation(groupInfo.UpdateInGroup);
var createGroup = CreateGroupGenerator.GetTryGetCreateGroup(in groupInfo);
Expand All @@ -290,6 +305,8 @@ public static string GetGroupPartialClass(in GroupInfo groupInfo)
{{groupInfo.AccessModifier}} partial class {{groupInfo.ClassName}}{{(customBehaviour ? string.Empty : ": DefaultGroup<float>")}} {
{{addEdgesFieldDeclaration}}
{{validateEdgesFieldDeclaration}}
public static void TryCreateGroup<T>(ref ArchSystemsWorldBuilder<T> worldBuilder)
{
Expand All @@ -301,6 +318,11 @@ private static void AddEdges(Dictionary<Type, List<Type>> edgesMap)
{
{{addEdgesBody}}
}
private static void {{EdgesGenerator.ValidateEdgesMethodName}}(List<DisconnectedDependenciesInfo.WrongTypeBinding> {{EdgesGenerator.DisconnectedDependenciesFieldName}})
{
{{validateEdgesBody}}
}
{{metadata}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Arch.SystemGroups.Tests.TestSetup1;
using NSubstitute;

namespace Arch.SystemGroups.Tests.DisconnectedDependencies;

public class DisconnectedDependenciesTests
{
private ArchSystemsWorldBuilder<TestWorld> _worldBuilder;

[SetUp]
public void SetUp()
{
_worldBuilder = new ArchSystemsWorldBuilder<TestWorld>(new TestWorld(), Substitute.For<IPlayerLoop>());
}

[Test]
public void ThrowsOnSystems()
{
DDSystem1.InjectToWorld(ref _worldBuilder);
DDSystem2.InjectToWorld(ref _worldBuilder);

Assert.Throws<DisconnectedDependenciesFoundException>(() => _worldBuilder.Finish());
}

[Test]
public void ThrowsOnGroups()
{
DDSystem1Gr1.InjectToWorld(ref _worldBuilder);
DDSystem1Gr2.InjectToWorld(ref _worldBuilder);

Assert.Throws<DisconnectedDependenciesFoundException>(() => _worldBuilder.Finish());
}
}
16 changes: 16 additions & 0 deletions Arch.SystemGroups.Tests/DisconnectedDependencies/Groups.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Arch.SystemGroups.DefaultSystemGroups;

namespace Arch.SystemGroups.Tests.DisconnectedDependencies;

[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class DDGroup1
{

}

[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(DDGroup1))]
public partial class DDGroup2
{

}
38 changes: 38 additions & 0 deletions Arch.SystemGroups.Tests/DisconnectedDependencies/Systems.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Arch.System;
using Arch.SystemGroups.DefaultSystemGroups;
using Arch.SystemGroups.Tests.TestSetup1;

namespace Arch.SystemGroups.Tests.DisconnectedDependencies;

[UpdateInGroup(typeof(PostRenderingSystemGroup))]
[UpdateAfter(typeof(DDSystem2))] // exception
public partial class DDSystem1 : BaseSystem<TestWorld, float>
{
public DDSystem1(TestWorld world) : base(world)
{
}
}

[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial class DDSystem2 : BaseSystem<TestWorld, float>
{
public DDSystem2(TestWorld world) : base(world)
{
}
}

[UpdateInGroup(typeof(DDGroup1))]
public partial class DDSystem1Gr1 : BaseSystem<TestWorld, float>
{
public DDSystem1Gr1(TestWorld world) : base(world)
{
}
}

[UpdateInGroup(typeof(DDGroup2))]
public partial class DDSystem1Gr2 : BaseSystem<TestWorld, float>
{
public DDSystem1Gr2(TestWorld world) : base(world)
{
}
}
29 changes: 29 additions & 0 deletions Arch.SystemGroups.Tests/GenericSetup/NestedArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Arch.System;
using Arch.SystemGroups.DefaultSystemGroups;
using Arch.SystemGroups.Tests.TestSetup1;

namespace Arch.SystemGroups.Tests.GenericSetup;

public class ParentClass<T>
{
public class NestedType
{
public T Value;
}
}

public struct ParamGen
{
public bool Value;
}

[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial class SystemWithGenericNestedArgument : BaseSystem<TestWorld, float>
{
private readonly ParentClass<ParamGen>.NestedType _param;

internal SystemWithGenericNestedArgument(TestWorld world, ParentClass<ParamGen>.NestedType param) : base(world)
{
_param = param;
}
}
8 changes: 4 additions & 4 deletions Arch.SystemGroups.Tests/TestSetup2/Groups/GroupsB.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ public partial class GroupBB

[UpdateInGroup(typeof(GroupBA))]
// This dependency should not be taken into consideration even as
// this group does belong to the GroupBB hierarchy.
[UpdateAfter(typeof(GroupBB))]
// this group does not belong to the GroupBB hierarchy.
// [UpdateAfter(typeof(GroupBB))] no tolerance
public partial class GroupBAA
{
}

[UpdateInGroup(typeof(GroupBA))]
[UpdateAfter(typeof(GroupBAA))]
// This dependency should not be taken into consideration even as
// this group does belong to the A hierarchy.
[UpdateBefore(typeof(GroupAA))]
// this group does not belong to the A hierarchy.
// [UpdateBefore(typeof(GroupAA))] no tolerance
public partial class GroupBAB
{
}
Expand Down
2 changes: 1 addition & 1 deletion Arch.SystemGroups.Tests/TestSetup2/Systems/SystemsA.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public SystemCGroupAA(TestWorld2 world) : base(world)
// Group AB

[UpdateInGroup(typeof(GroupAB))]
[UpdateBefore(typeof(SystemCGroupAA))] // < -- ignore
// [UpdateBefore(typeof(SystemCGroupAA))] // no tolerance
[UpdateBefore(typeof(SystemCGroupAB))]
public partial class SystemAGroupAB : BaseSystem<TestWorld2, float>
{
Expand Down
8 changes: 4 additions & 4 deletions Arch.SystemGroups.Tests/TestSetup2/Systems/SystemsB.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ public SystemDGroupBA(TestWorld2 world) : base(world)
[UpdateInGroup(typeof(GroupBB))]
[UpdateBefore(typeof(SystemBGroupBB))]
// The following edges will be ignored as belong to a different group
[UpdateAfter(typeof(SystemCGroupBA))]
[UpdateAfter(typeof(SystemBGroupBA))]
[UpdateAfter(typeof(SystemAGroupBA))]
[UpdateAfter(typeof(SystemDGroupBA))]
// [UpdateAfter(typeof(SystemCGroupBA))] no tolerance
// [UpdateAfter(typeof(SystemBGroupBA))]
// [UpdateAfter(typeof(SystemAGroupBA))]
// [UpdateAfter(typeof(SystemDGroupBA))]
public partial class SystemAGroupBB : BaseSystem<TestWorld2, float>
{
public SystemAGroupBB(TestWorld2 world) : base(world)
Expand Down
3 changes: 2 additions & 1 deletion Arch.SystemGroups/Arch.SystemGroups.csproj.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=builder/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=groups/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playerloophelper/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playerloophelper_005Caggregation/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=playerloophelper_005Caggregation/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=systemsdependencies/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
Loading

0 comments on commit aac53ea

Please sign in to comment.