Skip to content

Commit

Permalink
feat: DependsOnContext
Browse files Browse the repository at this point in the history
  • Loading branch information
bdunderscore committed Nov 18, 2024
1 parent 5aeab25 commit 05c9a7e
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- [#472] Added the `DependsOnContext` attribute, for declaring dependencies between extension contexts.

### Fixed

Expand Down
20 changes: 20 additions & 0 deletions Editor/API/Attributes/DependsOnContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace nadena.dev.ndmf
{
/// <summary>
/// This attribute declares a pass or an extension context to depend on another context.
/// When an extension context depends on another, it will implicitly activate the other context whenever the
/// depending context is activated.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class DependsOnContext : Attribute
{
public Type ExtensionContext { get; }

public DependsOnContext(Type extensionContext)
{
ExtensionContext = extensionContext;
}
}
}
3 changes: 3 additions & 0 deletions Editor/API/Attributes/DependsOnContext.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 52 additions & 1 deletion Editor/API/IExtensionContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
namespace nadena.dev.ndmf
using System;
using System.Collections.Generic;

namespace nadena.dev.ndmf
{
/// <summary>
/// The IExtensionContext is declared by custom extension contexts.
Expand All @@ -17,4 +20,52 @@ public interface IExtensionContext
/// <param name="context"></param>
void OnDeactivate(BuildContext context);
}

internal static class ExtensionContextUtil
{
public static IEnumerable<Type> ContextDependencies(this Type ty, bool recurse)
{
if (recurse)
{
return RecursiveContextDependencies(ty);
}

return ContextDependencies(ty);
}

public static IEnumerable<Type> ContextDependencies(this Type ty)
{
foreach (var attr in ty.GetCustomAttributes(typeof(DependsOnContext), true))
{
if (attr is DependsOnContext dependsOn && dependsOn.ExtensionContext != null)
{
yield return dependsOn.ExtensionContext;
}
}
}

private static IEnumerable<Type> RecursiveContextDependencies(Type ty)
{
HashSet<Type> enqueued = new();
Queue<Type> queue = new();

queue.Enqueue(ty);
enqueued.Add(ty);

while (queue.Count > 0)
{
var current = queue.Dequeue();

yield return current;

foreach (var dep in ContextDependencies(current))
{
if (enqueued.Add(dep))
{
queue.Enqueue(dep);
}
}
}
}
}
}
23 changes: 20 additions & 3 deletions Editor/API/Model/SolverPass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using nadena.dev.ndmf.preview;

#endregion
Expand All @@ -26,9 +27,25 @@ internal class SolverPass
internal IImmutableSet<string> CompatibleExtensions { get; set; }
internal List<IRenderFilter> RenderFilters { get; } = new();

internal bool IsExtensionCompatible(Type ty)
internal bool IsExtensionCompatible(Type ty, ISet<Type> activeExtensions)
{
return IsPhantom || RequiredExtensions.Contains(ty) || CompatibleExtensions.Contains(ty.FullName);
if (IsPhantom || RequiredExtensions.Contains(ty) || CompatibleExtensions.Contains(ty.FullName))
{
return true;
}

// See if any of the active extensions depends on the given type, and if so, if we are compatible with it.
foreach (var active in activeExtensions)
{
if (!CompatibleExtensions.Contains(active.FullName) && !RequiredExtensions.Contains(active))
{
continue;
}

if (active.ContextDependencies(true).Contains(ty)) return true;
}

return false;
}

internal SolverPass(IPluginInternal plugin, IPass pass, BuildPhase phase, IImmutableSet<string> compatibleExtensions,
Expand All @@ -37,8 +54,8 @@ internal SolverPass(IPluginInternal plugin, IPass pass, BuildPhase phase, IImmut
Plugin = plugin;
Pass = pass;
Phase = phase;
RequiredExtensions = requiredExtensions;
CompatibleExtensions = compatibleExtensions;
RequiredExtensions = requiredExtensions.Union(pass.GetType().ContextDependencies());
}

public override string ToString()
Expand Down
40 changes: 38 additions & 2 deletions Editor/API/Solver/PluginResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ ImmutableList<ConcretePass> ToConcretePasses(BuildPhase phase, IEnumerable<Solve
var toActivate = new List<Type>();
activeExtensions.RemoveWhere(t =>
{
if (!pass.IsExtensionCompatible(t))
if (!pass.IsExtensionCompatible(t, activeExtensions))
{
toDeactivate.Add(t);
return true;
Expand All @@ -191,7 +191,7 @@ ImmutableList<ConcretePass> ToConcretePasses(BuildPhase phase, IEnumerable<Solve
return false;
});

foreach (var t in pass.RequiredExtensions.ToImmutableSortedSet(new TypeComparer()))
foreach (var t in ResolveExtensionDependencies(pass.RequiredExtensions))
{
if (!activeExtensions.Contains(t))
{
Expand Down Expand Up @@ -222,6 +222,42 @@ ImmutableList<ConcretePass> ToConcretePasses(BuildPhase phase, IEnumerable<Solve
return concrete.ToImmutableList();
}

private IEnumerable<Type> ResolveExtensionDependencies(IImmutableSet<Type> passRequiredExtensions)
{
var resultSet = new HashSet<Type>();
var results = new List<Type>();
var stack = new Stack<Type>();

foreach (var type in new SortedSet<Type>(passRequiredExtensions, new TypeComparer()))
{
VisitType(type);
}

return results;

void VisitType(Type ty)
{
if (stack.Contains(ty))
{
throw new Exception("Circular dependency detected: " + string.Join(" -> ", stack));
}

if (resultSet.Contains(ty)) return;

stack.Push(ty);

foreach (var dep in new SortedSet<Type>(ty.ContextDependencies(), new TypeComparer()))
{
VisitType(dep);
}

stack.Pop();

resultSet.Add(ty);
results.Add(ty);
}
}

internal PreviewSession PreviewSession
{
get
Expand Down
88 changes: 88 additions & 0 deletions UnitTests~/PluginResolverTests/ExtensionDependenciesTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Linq;
using nadena.dev.ndmf;
using NUnit.Framework;

namespace UnitTests.PluginResolverTests
{
[DependsOnContext(typeof(Ctx3))]
[DependsOnContext(typeof(Ctx2))]
public class Ctx1 : IExtensionContext
{
public void OnActivate(BuildContext context)
{

}

public void OnDeactivate(BuildContext context)
{

}
}

public class Ctx2 : IExtensionContext
{
public void OnActivate(BuildContext context)
{

}

public void OnDeactivate(BuildContext context)
{

}
}

public class Ctx3 : IExtensionContext
{
public void OnActivate(BuildContext context)
{

}

public void OnDeactivate(BuildContext context)
{

}
}

[DependsOnContext(typeof(Ctx1))]
public class Pass1 : Pass<Pass1>
{
protected override void Execute(BuildContext context)
{

}
}

public class Plugin1 : Plugin<Plugin1>
{
protected override void Configure()
{
InPhase(BuildPhase.Generating)
.Run(Pass1.Instance)
.Then.WithCompatibleExtension(typeof(Ctx1), seq =>
{
seq.Run("test test", _ => { });
});
}
}

public class ExtensionDependenciesTest
{
[Test]
public void AssertCorrectPassDependencies()
{
var resolver = new PluginResolver(new[] { typeof(Plugin1) });

var phase = resolver.Passes.First(p => p.Item1 == BuildPhase.Generating).Item2;
var pass1 = phase.First(pass => pass.InstantiatedPass is Pass1);

Assert.That(pass1.ActivatePlugins, Is.EquivalentTo(new[] { typeof(Ctx2), typeof(Ctx3), typeof(Ctx1) }));
Assert.That(pass1.DeactivatePlugins, Is.Empty);

var pass2 = phase.First(pass => pass.InstantiatedPass.DisplayName == "test test");
Assert.That(pass2.ActivatePlugins.IsEmpty);
Assert.That(pass2.DeactivatePlugins.IsEmpty);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 05c9a7e

Please sign in to comment.