Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions src/Pure.DI.Core/Core/DependencyGraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public IEnumerable<DependencyNode> Build(GraphBuildContext ctx)
var edgesMap = new Dictionary<IProcessingNode, List<Dependency>>();
var injectionPositionComparer = new InjectionPositionComparer(injectionComparer);
var handledInjections = new Dictionary<IProcessingNode, HashSet<(Injection Injection, int? Position)>>();
var genericCandidates = new List<GenericCandidate>();
var counter = 0;
while (queue.Count > 0)
{
Expand Down Expand Up @@ -207,23 +208,22 @@ public IEnumerable<DependencyNode> Build(GraphBuildContext ctx)
when namedTypeSymbol.IsGenericType || marker.IsMarkerBased(setup, namedTypeSymbol):
{
var isDone = false;
foreach (var item in map.OrderByDescending(i => i.Value.Binding.Id))
// Candidates are tried from the newest binding down, in map order among equal ids.
// The map is filtered before it is ordered, into a list reused for every injection.
genericCandidates.Clear();
foreach (var candidate in map)
{
if (item.Key.Type is not INamedTypeSymbol nextNamedTypeSymbol)
if (candidate.Key.Type is INamedTypeSymbol nextNamedTypeSymbol
&& nextNamedTypeSymbol.IsGenericType == namedTypeSymbol.IsGenericType
&& Injection.EqualTags(injection.Tag, candidate.Key.Tag))
{
continue;
}

if (nextNamedTypeSymbol.IsGenericType != namedTypeSymbol.IsGenericType)
{
continue;
}

if (!Injection.EqualTags(injection.Tag, item.Key.Tag))
{
continue;
genericCandidates.Add(new GenericCandidate(genericCandidates.Count, candidate));
}
}

genericCandidates.Sort(GenericCandidate.NewestFirst);
foreach (var (_, item) in genericCandidates)
{
if (!typeConstructor.TryBind(setup, item.Key.Type, injection.Type))
{
continue;
Expand Down Expand Up @@ -902,4 +902,14 @@ internal readonly struct ConstructKindKey(INamedTypeSymbol typeSymbol, ITypeSymb

public override int GetHashCode() => typeSymbolComparer.GetRuntimeHashCode(TypeSymbol);
}

// The position keeps map order among equal binding ids, as List.Sort is not stable.
private readonly record struct GenericCandidate(int Position, KeyValuePair<Injection, DependencyNode> Item)
{
public static readonly Comparison<GenericCandidate> NewestFirst = (x, y) =>
{
var byId = y.Item.Value.Binding.Id.CompareTo(x.Item.Value.Binding.Id);
return byId != 0 ? byId : x.Position.CompareTo(y.Position);
};
}
}
77 changes: 77 additions & 0 deletions src/Pure.DI.Core/Core/FactoryRewriteCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// ReSharper disable ClassNeverInstantiated.Global

namespace Pure.DI.Core;

using System.Runtime.CompilerServices;

// A generic factory is rewritten for every closed type it is resolved with, over the same syntax.
// Per generation, this keeps the type each name of a factory binds to and the syntax it is rewritten to.
// Its keys hold syntax nodes and semantic models, so it must not outlive the generation.
sealed class FactoryRewriteCache : IFactoryRewriteCache
{
private readonly Dictionary<SymbolKey, ITypeSymbol?> _typeSymbols = new();
private readonly Dictionary<TypeSyntaxKey, TypeSyntax> _typeSyntaxes = new();

public ITypeSymbol? GetTypeSymbol(SemanticModel semanticModel, SyntaxNode node)
{
var key = new SymbolKey(semanticModel, node);
if (!_typeSymbols.TryGetValue(key, out var type))
{
type = semanticModel.GetSymbolInfo(node).Symbol as ITypeSymbol;
_typeSymbols.Add(key, type);
}

return type;
}

// Syntax nodes are immutable, so one node serves every rewrite of the same name to the same type.
public TypeSyntax GetTypeSyntax(SyntaxNode node, string typeName)
{
var key = new TypeSyntaxKey(node, typeName);
if (!_typeSyntaxes.TryGetValue(key, out var typeSyntax))
{
typeSyntax = SyntaxFactory.ParseTypeName(typeName)
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithTrailingTrivia(node.GetTrailingTrivia());
_typeSyntaxes.Add(key, typeSyntax);
}

return typeSyntax;
}

private readonly struct SymbolKey(SemanticModel semanticModel, SyntaxNode node) : IEquatable<SymbolKey>
{
private readonly SemanticModel _semanticModel = semanticModel;
private readonly SyntaxNode _node = node;

public bool Equals(SymbolKey other) => ReferenceEquals(_semanticModel, other._semanticModel) && ReferenceEquals(_node, other._node);

public override bool Equals(object? obj) => obj is SymbolKey other && Equals(other);

public override int GetHashCode()
{
unchecked
{
return RuntimeHelpers.GetHashCode(_semanticModel) * 397 ^ RuntimeHelpers.GetHashCode(_node);
}
}
}

private readonly struct TypeSyntaxKey(SyntaxNode node, string typeName) : IEquatable<TypeSyntaxKey>
{
private readonly SyntaxNode _node = node;
private readonly string _typeName = typeName;

public bool Equals(TypeSyntaxKey other) => ReferenceEquals(_node, other._node) && _typeName == other._typeName;

public override bool Equals(object? obj) => obj is TypeSyntaxKey other && Equals(other);

public override int GetHashCode()
{
unchecked
{
return RuntimeHelpers.GetHashCode(_node) * 397 ^ _typeName.GetHashCode();
}
}
}
}
9 changes: 4 additions & 5 deletions src/Pure.DI.Core/Core/FactoryTypeRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ namespace Pure.DI.Core;
sealed class FactoryTypeRewriter(
IMarker marker,
ITypeResolver typeResolver,
ITypes types)
ITypes types,
IFactoryRewriteCache cache)
: CSharpSyntaxRewriter, IFastBuilder<RewriterContext<MdFactory>, MdFactory>
{
private RewriterContext<MdFactory> _context;
Expand Down Expand Up @@ -53,9 +54,7 @@ public MdFactory Build(in RewriterContext<MdFactory> context)

private SyntaxNode? TryCreateTypeSyntax(SyntaxNode node) =>
TryGetNewTypeName(node, !IsObjectCreationType(node), out var newTypeName)
? SyntaxFactory.ParseTypeName(newTypeName)
.WithLeadingTrivia(node.GetLeadingTrivia())
.WithTrailingTrivia(node.GetTrailingTrivia())
? cache.GetTypeSyntax(node, newTypeName)
: null;

private bool TryGetNewTypeName(SyntaxNode? node, bool includeNullableAnnotation, [NotNullWhen(true)] out string? newTypeName)
Expand All @@ -69,7 +68,7 @@ private bool TryGetNewTypeName(SyntaxNode? node, bool includeNullableAnnotation,
var semanticModel = _context.State.SemanticModel;
if (node.SyntaxTree == semanticModel.SyntaxTree)
{
if (semanticModel.GetSymbolInfo(node).Symbol is ITypeSymbol type)
if (cache.GetTypeSymbol(semanticModel, node) is {} type)
{
return TryGetNewTypeName(type, true, includeNullableAnnotation, out newTypeName);
}
Expand Down
8 changes: 8 additions & 0 deletions src/Pure.DI.Core/Core/IFactoryRewriteCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Pure.DI.Core;

interface IFactoryRewriteCache
{
ITypeSymbol? GetTypeSymbol(SemanticModel semanticModel, SyntaxNode node);

TypeSyntax GetTypeSyntax(SyntaxNode node, string typeName);
}
2 changes: 1 addition & 1 deletion src/Pure.DI.Core/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private void Setup() => DI.Setup()

// PerResolve
.PerResolve<TypeResolver, RootSignatureProvider, LogObserver, Types, Filter, Registry<TT>, Locks, RootAccessModifierResolver, SmartTags, GenericTypeArguments,
NameProvider, OverrideIdProvider, OverridesRegistry, Accumulators, Constructors>()
NameProvider, OverrideIdProvider, OverridesRegistry, Accumulators, Constructors, FactoryRewriteCache>()

// Id generators
.PerResolve<IdGenerator>(UniqueTagIdGenerator)
Expand Down
103 changes: 103 additions & 0 deletions tests/Pure.DI.Tests/FactoryRewriteCacheTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
namespace Pure.DI.Tests;

using Core;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

public class FactoryRewriteCacheTests
{
private const string Code = """
class Dependency { }

class Factory
{
object Create(int count)
{
Dependency value = new Dependency();
return count > 0 ? value : null;
}
}
""";

[Fact]
public void ShouldProvideTypeOfName()
{
// Given
var (semanticModel, root) = Compile();
var name = FindName(root, "Dependency");
var cache = new FactoryRewriteCache();

// When
var type = cache.GetTypeSymbol(semanticModel, name);

// Then
type.ShouldNotBeNull();
type.Name.ShouldBe("Dependency");
cache.GetTypeSymbol(semanticModel, name).ShouldBeSameAs(type);
}

[Fact]
public void ShouldProvideNoTypeForNameOfValue()
{
// Given
var (semanticModel, root) = Compile();
var name = FindName(root, "count");
var cache = new FactoryRewriteCache();

// When
var type = cache.GetTypeSymbol(semanticModel, name);

// Then
type.ShouldBeNull();
cache.GetTypeSymbol(semanticModel, name).ShouldBeNull();
}

[Fact]
public void ShouldReuseTypeSyntaxForSameNameAndType()
{
// Given
var (_, root) = Compile();
var name = FindName(root, "Dependency");
var cache = new FactoryRewriteCache();

// When
var typeSyntax = cache.GetTypeSyntax(name, "global::Other");

// Then
typeSyntax.ToFullString().ShouldBe(name.ToFullString().Replace("Dependency", "global::Other"));
cache.GetTypeSyntax(name, "global::Other").ShouldBeSameAs(typeSyntax);
}

[Fact]
public void ShouldBuildTypeSyntaxForEachType()
{
// Given
var (_, root) = Compile();
var name = FindName(root, "Dependency");
var cache = new FactoryRewriteCache();

// When
var first = cache.GetTypeSyntax(name, "global::First");
var second = cache.GetTypeSyntax(name, "global::Second");

// Then
first.ToString().ShouldBe("global::First");
second.ToString().ShouldBe("global::Second");
}

private static (SemanticModel SemanticModel, SyntaxNode Root) Compile()
{
var tree = CSharpSyntaxTree.ParseText(Code);
var compilation = CSharpCompilation.Create(
nameof(FactoryRewriteCacheTests),
[tree],
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]);

return (compilation.GetSemanticModel(tree), tree.GetRoot());
}

// The first occurrence: for "Dependency", the type of the local variable.
private static IdentifierNameSyntax FindName(SyntaxNode root, string name) =>
root.DescendantNodes().OfType<IdentifierNameSyntax>().First(i => i.Identifier.Text == name);
}
Loading