Skip to content

Commit

Permalink
#32 Lazy cyclic dependencies are no longer possible in v2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikolay Pyanikov committed Aug 11, 2023
1 parent a4d29f2 commit 35af751
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 48 deletions.
2 changes: 2 additions & 0 deletions src/Pure.DI.Core/Composition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ private static void Setup() => DI.Setup(nameof(Composition))
.Bind<IBuilder<IEnumerable<SyntaxUpdate>, Unit>>().To<Core.Generator>()
.Bind<IBuilder<RewriterContext<MdFactory>, MdFactory>>().To<FactoryTypeRewriter>()
.Bind<IBuilder<DependencyGraph, CompositionCode>>(WellknownTag.CompositionBuilder).To<CompositionBuilder>()
.Bind<IGraphPath>().To<GraphPath>()

// PerResolve
.DefaultLifetime(Lifetime.PerResolve)
Expand All @@ -56,6 +57,7 @@ private static void Setup() => DI.Setup(nameof(Composition))
.Bind<IValidator<DependencyGraph>>().To<DependencyGraphValidator>()
.Bind<IValidator<MdSetup>>().To<MetadataValidator>()
.Bind<IApiInvocationProcessor>().To<ApiInvocationProcessor>()
.Bind<IPathfinder>().To<Pathfinder>()
.Bind<IBuilder<LogEntry, LogInfo>>().To<LogInfoBuilder>()
.Bind<IBuilder<ImmutableArray<Root>, IEnumerable<ResolverInfo>>>().To<ResolversBuilder>()
.Bind<IBuilder<ContractsBuildContext, ISet<Injection>>>().To<ContractsBuilder>()
Expand Down
8 changes: 6 additions & 2 deletions src/Pure.DI.Core/Core/Code/CompositionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ protected override void VisitConstructor(

var hasRequired = required.Any();
context.Code.AppendLines(GenerateDeclareStatements(instantiation.Target, newStatement, hasRequired ? "" : ";"));
instantiation.Target.IsDeclared = true;
if (hasRequired)
{
context.Code.AppendLine("{");
Expand Down Expand Up @@ -332,6 +333,7 @@ protected override void VisitCompositionConstruct(
}

context.Code.AppendLines(GenerateDeclareStatements(instantiation.Target, "this"));
instantiation.Target.IsDeclared = true;
AddReturnStatement(context, root, instantiation);
instantiation.Target.IsCreated = true;
}
Expand All @@ -344,6 +346,7 @@ protected override void VisitOnCannotResolve(BuildContext context, Variable root
}

context.Code.AppendLines(GenerateDeclareStatements(instantiation.Target, $"{Constant.OnCannotResolve}<{instantiation.Target.ContractType}>({instantiation.Target.Injection.Tag.ValueToString()}, {instantiation.Target.Node.Lifetime.ValueToString()})"));
instantiation.Target.IsDeclared = true;
context.Code.AppendLines(GenerateOnInstanceCreatedStatements(context, instantiation.Target));
AddReturnStatement(context, root, instantiation);
instantiation.Target.IsCreated = true;
Expand Down Expand Up @@ -376,7 +379,8 @@ protected override void VisitFactory(
var code = context.Code;
if (!instantiation.Target.IsDeclared)
{
code.AppendLine($"{instantiation.Target.InstanceType} {instantiation.Target.Name};");
code.AppendLine($"{instantiation.Target.InstanceType} {instantiation.Target.Name} = default({instantiation.Target.InstanceType});");
instantiation.Target.IsDeclared = true;
}

var factoryBuildContext = context with
Expand All @@ -390,6 +394,7 @@ protected override void VisitFactory(
code.Append($"{instantiation.Target.Name} = ");
}

instantiation.Target.IsCreated = true;
RewriteFactory(factoryBuildContext, context.IsRootContext, dependencyGraph, instantiation, factory, cancellationToken);
code.AppendLines(GenerateOnInstanceCreatedStatements(context, instantiation.Target));

Expand All @@ -399,7 +404,6 @@ protected override void VisitFactory(
code.AppendLines(GenerateReturnStatements(context, instantiation.Target));
}

instantiation.Target.IsCreated = true;
base.VisitFactory(context, dependencyGraph, root, instantiation, factory, cancellationToken);
}

Expand Down
66 changes: 21 additions & 45 deletions src/Pure.DI.Core/Core/DependencyGraphValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ namespace Pure.DI.Core;
internal sealed class DependencyGraphValidator: IValidator<DependencyGraph>
{
private readonly ILogger<DependencyGraphValidator> _logger;
private readonly IPathfinder _pathfinder;
private readonly Func<IGraphPath> _pathFactory;
private readonly CancellationToken _cancellationToken;

public DependencyGraphValidator(
ILogger<DependencyGraphValidator> logger,
IPathfinder pathfinder,
Func<IGraphPath> pathFactory,
CancellationToken cancellationToken)
{
_logger = logger;
_pathfinder = pathfinder;
_pathFactory = pathFactory;
_cancellationToken = cancellationToken;
}

Expand All @@ -35,67 +41,37 @@ public void Validate(in DependencyGraph dependencyGraph)
}
}

var cycles = new List<(Dependency CyclicDependency, ImmutableArray<DependencyNode> Path)>();
using (_logger.TraceProcess("search for cycles"))
{
var paths = new Dictionary<int, IGraphPath>();
foreach (var rootNode in dependencyGraph.Roots.Select(i => i.Value.Node))
{
_cancellationToken.ThrowIfCancellationRequested();
if (!graph.TryGetInEdges(rootNode, out var dependencies))
{
continue;
}

var path = new LinkedList<DependencyNode>();
path.AddLast(rootNode);
var ids = new HashSet<int>();
var enumerators = new Stack<(int Id, IEnumerator<Dependency> Enumerator)>();
enumerators.Push((rootNode.Binding.Id, dependencies.GetEnumerator()));
while (enumerators.TryPop(out var enumerator))
foreach (var (id, dependency) in _pathfinder.GetPaths(graph, rootNode))
{
_cancellationToken.ThrowIfCancellationRequested();
if (!enumerator.Enumerator.MoveNext())
if (!paths.TryGetValue(id, out var path))
{
if (path.Count > 0)
{
path.RemoveLast();
}

ids.Remove(enumerator.Id);
continue;
path = _pathFactory();
paths.Add(id, path);
}

var dependency = enumerator.Enumerator.Current;
if (!ids.Add(dependency.Source.Binding.Id))

if (!path.TryAddPart(dependency.Target))
{
cycles.Add((dependency, path.ToImmutableArray()));
_logger.CompileError($"A cyclic dependency has been found: {path}.", dependencyGraph.Source.Source.GetLocation(), LogId.ErrorCyclicDependency);
isErrorReported = true;
isValid = false;
break;
}

path.AddLast(dependency.Source);
enumerators.Push(enumerator);
if (!graph.TryGetInEdges(dependency.Source, out var nestedDependencies))
if (path.IsCompleted(dependency.Target))
{
continue;
break;
}

enumerators.Push((dependency.Source.Binding.Id, nestedDependencies.GetEnumerator()));
}

paths.Clear();
}
}

if (cycles.Any())
{
isValid = false;
foreach (var cycle in cycles)
{
_cancellationToken.ThrowIfCancellationRequested();
var pathDescription = string.Join(" <-- ", cycle.Path.Select(i => $"{i.Type}"));
_logger.CompileError($"A cyclic dependency has been found {pathDescription}.", dependencyGraph.Source.Source.GetLocation(), LogId.ErrorCyclicDependency);
isErrorReported = true;
}
}


if (isValid)
{
return;
Expand Down
2 changes: 1 addition & 1 deletion src/Pure.DI.Core/Core/FileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ public void CreateDirectory(string path) =>
Directory.CreateDirectory(path);

public string? GetDirectoryName(string path) =>
Path.GetDirectoryName(path);
System.IO.Path.GetDirectoryName(path);
}
33 changes: 33 additions & 0 deletions src/Pure.DI.Core/Core/GraphPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Pure.DI.Core;

internal class GraphPath : IGraphPath
{
private readonly List<DependencyNode> _parts = new();
private readonly HashSet<MdBinding> _bindings = new();
private HashSet<MdBinding> _prevBindings = new();

public bool TryAddPart(in DependencyNode node)
{
_parts.Add(node);

// ReSharper disable once InvertIf
if (node.Type is INamedTypeSymbol { IsGenericType: true, Name: "Func" } namedType
&& namedType.ContainingNamespace.Name == "System")
{
LazyBarrier();
}

return _bindings.Add(node.Binding);
}

public bool IsCompleted(in DependencyNode node) =>
_prevBindings.Contains(node.Binding);

private void LazyBarrier()
{
_prevBindings = new HashSet<MdBinding>(_bindings);
_bindings.Clear();
}

public override string ToString() => string.Join(" <-- ", _parts.Select(i => i.Type));
}
7 changes: 7 additions & 0 deletions src/Pure.DI.Core/Core/IGraphPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Pure.DI.Core;

internal interface IGraphPath
{
bool TryAddPart(in DependencyNode node);
bool IsCompleted(in DependencyNode node);
}
8 changes: 8 additions & 0 deletions src/Pure.DI.Core/Core/IPathfinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Pure.DI.Core;

internal interface IPathfinder
{
IEnumerable<(int pathId, Dependency dependency)> GetPaths(
IGraph<DependencyNode, Dependency> graph,
DependencyNode rootNode);
}
44 changes: 44 additions & 0 deletions src/Pure.DI.Core/Core/Pathfinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace Pure.DI.Core;

internal class Pathfinder : IPathfinder
{
private readonly CancellationToken _cancellationToken;

public Pathfinder(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
}

public IEnumerable<(int pathId, Dependency dependency)> GetPaths(
IGraph<DependencyNode, Dependency> graph,
DependencyNode rootNode)
{
if (!graph.TryGetInEdges(rootNode, out var dependencies))
{
yield break;
}

var id = 0;
var enumerators = new Stack<(int Id, IEnumerator<Dependency> Enumerator)>();
enumerators.Push((rootNode.Binding.Id, dependencies.GetEnumerator()));
while (enumerators.TryPop(out var enumerator))
{
_cancellationToken.ThrowIfCancellationRequested();
if (!enumerator.Enumerator.MoveNext())
{
id++;
continue;
}

var dependency = enumerator.Enumerator.Current;
yield return (id, dependency);
enumerators.Push(enumerator);
if (!graph.TryGetInEdges(dependency.Source, out var nestedDependencies))
{
continue;
}

enumerators.Push((dependency.Source.Binding.Id, nestedDependencies.GetEnumerator()));
}
}
}
70 changes: 70 additions & 0 deletions tests/Pure.DI.IntegrationTests/SetupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -605,4 +605,74 @@ public static void Main()
// Then
result.Success.ShouldBeTrue(result);
}

[Theory]
[InlineData("Func")]
[InlineData("Lazy")]
public async Task ShouldSupportLazyInjection(string lazyType)
{
// Given

// When
var result = await """
using System;
using Pure.DI;
namespace Sample
{
using Pure.DI;
public class Dependency : IDependency
{
public Dependency(Func<IDependency2> dependency2) { }
}
public interface IDependency
{
}
public class Dependency2 : IDependency2
{
public Dependency2(IDependency dependency) { }
}
public interface IDependency2
{
}
public interface IService
{
}
public class Service : IService
{
public Service(IDependency2 dependency2)
{
}
}
public partial class Composition
{
public static void Setup() =>
DI.Setup(nameof(Composition))
.Bind<IDependency>().To<Dependency>().Root<IDependency>("Dependency")
.Bind<IDependency2>().To<Dependency2>().Root<IDependency2>("Dependency2")
.Bind<IService>().To<Service>().Root<IService>("Service");
}
public class Program
{
public static void Main()
{
var composition = new Composition();
IService service = composition.Service;
}
}
}
""".Replace("Func<", lazyType + "<").RunAsync();

// Then
result.Success.ShouldBeTrue(result);
}
}
1 change: 1 addition & 0 deletions tests/Pure.DI.IntegrationTests/TestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ namespace Pure.DI.IntegrationTests;
using Microsoft.CodeAnalysis.Text;
using Moq;
using Generator = Generator;
using Path = System.IO.Path;

public static class TestExtensions
{
Expand Down

0 comments on commit 35af751

Please sign in to comment.