diff --git a/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs b/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs index ff97050ec..d22d9f351 100644 --- a/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs +++ b/src/Pure.DI.Core/Core/DependencyGraphBuilder.cs @@ -80,6 +80,7 @@ public IEnumerable Build(GraphBuildContext ctx) var edgesMap = new Dictionary>(); var injectionPositionComparer = new InjectionPositionComparer(injectionComparer); var handledInjections = new Dictionary>(); + var genericCandidates = new List(); var counter = 0; while (queue.Count > 0) { @@ -207,23 +208,22 @@ public IEnumerable 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; @@ -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 Item) + { + public static readonly Comparison 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); + }; + } } diff --git a/src/Pure.DI.Core/Core/FactoryRewriteCache.cs b/src/Pure.DI.Core/Core/FactoryRewriteCache.cs new file mode 100644 index 000000000..0e93f8236 --- /dev/null +++ b/src/Pure.DI.Core/Core/FactoryRewriteCache.cs @@ -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 _typeSymbols = new(); + private readonly Dictionary _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 + { + 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 + { + 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(); + } + } + } +} diff --git a/src/Pure.DI.Core/Core/FactoryTypeRewriter.cs b/src/Pure.DI.Core/Core/FactoryTypeRewriter.cs index 6c449da50..bbe87d025 100644 --- a/src/Pure.DI.Core/Core/FactoryTypeRewriter.cs +++ b/src/Pure.DI.Core/Core/FactoryTypeRewriter.cs @@ -6,7 +6,8 @@ namespace Pure.DI.Core; sealed class FactoryTypeRewriter( IMarker marker, ITypeResolver typeResolver, - ITypes types) + ITypes types, + IFactoryRewriteCache cache) : CSharpSyntaxRewriter, IFastBuilder, MdFactory> { private RewriterContext _context; @@ -53,9 +54,7 @@ public MdFactory Build(in RewriterContext 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) @@ -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); } diff --git a/src/Pure.DI.Core/Core/IFactoryRewriteCache.cs b/src/Pure.DI.Core/Core/IFactoryRewriteCache.cs new file mode 100644 index 000000000..d32836d9b --- /dev/null +++ b/src/Pure.DI.Core/Core/IFactoryRewriteCache.cs @@ -0,0 +1,8 @@ +namespace Pure.DI.Core; + +interface IFactoryRewriteCache +{ + ITypeSymbol? GetTypeSymbol(SemanticModel semanticModel, SyntaxNode node); + + TypeSyntax GetTypeSyntax(SyntaxNode node, string typeName); +} diff --git a/src/Pure.DI.Core/Generator.cs b/src/Pure.DI.Core/Generator.cs index 7f8f1ef0a..0945d4afc 100644 --- a/src/Pure.DI.Core/Generator.cs +++ b/src/Pure.DI.Core/Generator.cs @@ -122,7 +122,7 @@ private void Setup() => DI.Setup() // PerResolve .PerResolve, Locks, RootAccessModifierResolver, SmartTags, GenericTypeArguments, - NameProvider, OverrideIdProvider, OverridesRegistry, Accumulators, Constructors>() + NameProvider, OverrideIdProvider, OverridesRegistry, Accumulators, Constructors, FactoryRewriteCache>() // Id generators .PerResolve(UniqueTagIdGenerator) diff --git a/tests/Pure.DI.Tests/FactoryRewriteCacheTests.cs b/tests/Pure.DI.Tests/FactoryRewriteCacheTests.cs new file mode 100644 index 000000000..53e1a4a36 --- /dev/null +++ b/tests/Pure.DI.Tests/FactoryRewriteCacheTests.cs @@ -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().First(i => i.Identifier.Text == name); +}