Skip to content

Commit 0856552

Browse files
Fix simplified binding contract inference
1 parent d2dab57 commit 0856552

4 files changed

Lines changed: 243 additions & 4 deletions

File tree

‎src/Pure.DI.Core/Core/BindingBuilder.cs‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,19 @@ private IEnumerable<MdContract> CreateExplicitContractsFromImplementation(
155155
{
156156
var specialTypes = setup.SpecialTypes.Select(i => i.Type).ToImmutableHashSet(typeSymbolComparer.Runtime);
157157
var compilation = semanticModel.Compilation;
158+
var directInterfaces = GetDirectInterfaces(implementationType);
158159
baseSymbols = baseSymbolsProvider
159160
.GetBaseSymbols(implementationType, (type, deepness) => deepness switch
160161
{
161162
0 => true,
162163
1 => IsSuitableForBinding(compilation, specialTypes, type),
163164
_ => false
164165
}, 1)
165-
.Select(i => i.Type);
166+
.Select(i => i.Type)
167+
// For a type loaded from another assembly Roslyn may report transitive
168+
// InterfaceImpl entries in Interfaces. Keep the source-level semantics:
169+
// only interfaces implemented directly by the implementation are contracts.
170+
.Where(type => type.TypeKind != TypeKind.Interface || directInterfaces.Contains(type));
166171
}
167172

168173
var contracts = new HashSet<ITypeSymbol>(baseSymbols, typeSymbolComparer.Runtime) { implementationType };
@@ -178,13 +183,37 @@ private IEnumerable<MdContract> CreateExplicitContractsFromImplementation(
178183
}
179184
}
180185

186+
private ImmutableHashSet<ITypeSymbol> GetDirectInterfaces(ITypeSymbol implementationType)
187+
{
188+
var declaredInterfaces = implementationType.Interfaces;
189+
if (declaredInterfaces.IsDefaultOrEmpty)
190+
{
191+
return ImmutableHashSet<ITypeSymbol>.Empty.WithComparer(typeSymbolComparer.Runtime);
192+
}
193+
194+
var inheritedByBaseType = implementationType.BaseType?.AllInterfaces
195+
.ToImmutableHashSet(typeSymbolComparer.Runtime)
196+
?? ImmutableHashSet<ITypeSymbol>.Empty.WithComparer(typeSymbolComparer.Runtime);
197+
198+
return declaredInterfaces
199+
.Where(candidate => !inheritedByBaseType.Contains(candidate))
200+
.Where(candidate => !declaredInterfaces.Any(other =>
201+
!typeSymbolComparer.Runtime.Equals(other, candidate)
202+
&& other.AllInterfaces.Any(inherited => typeSymbolComparer.Runtime.Equals(inherited, candidate))))
203+
.ToImmutableHashSet(typeSymbolComparer.Runtime);
204+
}
205+
181206
private bool IsSuitableForBinding(Compilation compilation, ImmutableHashSet<ITypeSymbol> specialTypes, ITypeSymbol type)
182207
{
183208
// Checks if the type is an interface or an abstract class, which are typical candidates for DI contracts.
184209
var isAbstractOrInterface = type.TypeKind == TypeKind.Interface || type.IsAbstract;
185210

186-
// Ensures the type is not a predefined system type like 'object', 'string', or 'int' (SpecialType.None).
187-
var isNotSpecialType = type.SpecialType == Microsoft.CodeAnalysis.SpecialType.None;
211+
// For constructed generic types Roslyn may expose the special type only on the original
212+
// definition (for example IEnumerable<int> -> IEnumerable<T>).
213+
var specialType = type is INamedTypeSymbol namedType
214+
? namedType.OriginalDefinition.SpecialType
215+
: type.SpecialType;
216+
var isNotSpecialType = specialType == Microsoft.CodeAnalysis.SpecialType.None;
188217

189218
// 'System.IAsyncDisposable' is not covered by the Roslyn special types, so it is excluded explicitly
190219
// to keep it in line with 'System.IDisposable'.

‎tests/Pure.DI.IntegrationTests/Options.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ record Options(
55
NullableContextOptions NullableContextOptions = NullableContextOptions.Enable,
66
bool CheckCompilationErrors = true,
77
ImmutableArray<string> PreprocessorSymbols = default,
8-
string? Culture = "");
8+
string? Culture = "",
9+
ImmutableArray<MetadataReference> MetadataReferences = default);
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
namespace Pure.DI.IntegrationTests;
2+
3+
using Core;
4+
5+
public class SimplifiedBindingContractInferenceTests
6+
{
7+
public interface IMetadataBaseContract { }
8+
public interface IMetadataDerivedContract : IMetadataBaseContract { }
9+
public sealed class MetadataRouter : IMetadataBaseContract { }
10+
public sealed class MetadataRepository : IMetadataDerivedContract { }
11+
12+
[Fact]
13+
public async Task ShouldInferOnlyDirectContractsForTypesFromReferencedAssembly()
14+
{
15+
// When
16+
var result = await """
17+
using System;
18+
using Pure.DI;
19+
using IBaseContract = Pure.DI.IntegrationTests.SimplifiedBindingContractInferenceTests.IMetadataBaseContract;
20+
using Router = Pure.DI.IntegrationTests.SimplifiedBindingContractInferenceTests.MetadataRouter;
21+
using Repository = Pure.DI.IntegrationTests.SimplifiedBindingContractInferenceTests.MetadataRepository;
22+
23+
namespace Sample;
24+
25+
static class Setup
26+
{
27+
public static void Configure() => DI.Setup("BaseComposition", CompositionKind.Internal)
28+
.Singleton<Router, Repository>();
29+
}
30+
31+
partial class Composition
32+
{
33+
void Setup() => DI.Setup()
34+
.DependsOn("BaseComposition")
35+
.Root<IBaseContract>("Root");
36+
}
37+
38+
public class Program
39+
{
40+
public static void Main() =>
41+
Console.WriteLine(new Composition().Root.GetType().Name);
42+
}
43+
"""
44+
.RunAsync(new Options(
45+
LanguageVersion.Preview,
46+
MetadataReferences: [MetadataReference.CreateFromFile(typeof(SimplifiedBindingContractInferenceTests).Assembly.Location)]));
47+
48+
// Then
49+
result.Errors.ShouldBeEmpty(result);
50+
result.Warnings.ShouldBeEmpty(result);
51+
result.StdOut.ShouldBe(["MetadataRouter"], result);
52+
}
53+
54+
public static TheoryData<string> Bindings => new()
55+
{
56+
{
57+
"""
58+
.Bind().To<FirstService>()
59+
.Bind().To<SecondService>()
60+
.Bind().To<FirstConcreteService>()
61+
.Bind().To<SecondConcreteService>()
62+
"""
63+
},
64+
{
65+
"""
66+
.PerBlock<FirstService>()
67+
.PerBlock<SecondService>()
68+
.PerBlock<FirstConcreteService>()
69+
.PerBlock<SecondConcreteService>()
70+
"""
71+
},
72+
{
73+
"""
74+
.PerBlock<FirstService, SecondService, FirstConcreteService, SecondConcreteService>()
75+
"""
76+
},
77+
{
78+
"""
79+
.PerBlock<Dependency, FirstService>(dependency => new FirstService(dependency))
80+
.PerBlock<Dependency, SecondService>(dependency => new SecondService(dependency))
81+
.PerBlock<Dependency, FirstConcreteService>(dependency => new FirstConcreteService(dependency))
82+
.PerBlock<Dependency, SecondConcreteService>(dependency => new SecondConcreteService(dependency))
83+
"""
84+
}
85+
};
86+
87+
[Theory]
88+
[MemberData(nameof(Bindings))]
89+
public async Task ShouldInferTheSameContractsForAllSimplifiedBindingForms(string bindings)
90+
{
91+
// When
92+
var result = await """
93+
using System;
94+
using System.Collections;
95+
using System.Collections.Generic;
96+
using System.Threading.Tasks;
97+
using Pure.DI;
98+
99+
namespace Sample;
100+
101+
interface IInheritedFromDirectInterface { }
102+
interface IDirectAbstractContract : IInheritedFromDirectInterface { }
103+
interface IDirectConcreteContract { }
104+
interface ICustomSpecialContract { }
105+
interface IInheritedFromAbstractBase { }
106+
interface IInheritedFromConcreteBase { }
107+
108+
abstract class AbstractBase : IInheritedFromAbstractBase { }
109+
class ConcreteBase : IInheritedFromConcreteBase { }
110+
sealed class Dependency { }
111+
112+
sealed class FirstService :
113+
AbstractBase,
114+
IDirectAbstractContract,
115+
ICustomSpecialContract,
116+
IDisposable,
117+
IAsyncDisposable,
118+
IEnumerable<int>
119+
{
120+
public FirstService(Dependency dependency) { }
121+
public void Dispose() { }
122+
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
123+
public IEnumerator<int> GetEnumerator() => ((IEnumerable<int>)Array.Empty<int>()).GetEnumerator();
124+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
125+
}
126+
127+
sealed class SecondService :
128+
AbstractBase,
129+
IDirectAbstractContract,
130+
ICustomSpecialContract,
131+
IDisposable,
132+
IAsyncDisposable,
133+
IEnumerable<int>
134+
{
135+
public SecondService(Dependency dependency) { }
136+
public void Dispose() { }
137+
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
138+
public IEnumerator<int> GetEnumerator() => ((IEnumerable<int>)Array.Empty<int>()).GetEnumerator();
139+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
140+
}
141+
142+
sealed class FirstConcreteService : ConcreteBase, IDirectConcreteContract
143+
{
144+
public FirstConcreteService(Dependency dependency) { }
145+
}
146+
147+
sealed class SecondConcreteService : ConcreteBase, IDirectConcreteContract
148+
{
149+
public SecondConcreteService(Dependency dependency) { }
150+
}
151+
152+
partial class Composition
153+
{
154+
void Setup() => DI.Setup()
155+
.SpecialType<ICustomSpecialContract>()
156+
.Bind().To<Dependency>()
157+
#bindings#
158+
.Root<FirstService>("First")
159+
.Root<SecondService>("Second")
160+
.Root<FirstConcreteService>("FirstConcrete")
161+
.Root<SecondConcreteService>("SecondConcrete");
162+
}
163+
164+
public class Program
165+
{
166+
public static void Main()
167+
{
168+
var composition = new Composition();
169+
Console.WriteLine($"{composition.First.GetType().Name}," +
170+
$"{composition.Second.GetType().Name}," +
171+
$"{composition.FirstConcrete.GetType().Name}," +
172+
composition.SecondConcrete.GetType().Name);
173+
}
174+
}
175+
"""
176+
.Replace("#bindings#", bindings)
177+
.RunAsync(new Options(LanguageVersion.Preview));
178+
179+
// Then
180+
result.Errors.ShouldBeEmpty(result);
181+
result.StdOut.ShouldBe(["FirstService,SecondService,FirstConcreteService,SecondConcreteService"], result);
182+
183+
var overriddenContracts = result.Warnings
184+
.Where(i => i.Id == LogId.WarningOverriddenBinding)
185+
.Select(i => i.Message)
186+
.ToArray();
187+
188+
overriddenContracts.Length.ShouldBe(3, result);
189+
overriddenContracts.Count(i => i.Contains("AbstractBase", StringComparison.Ordinal)).ShouldBe(1, result);
190+
overriddenContracts.Count(i => i.Contains("IDirectAbstractContract", StringComparison.Ordinal)).ShouldBe(1, result);
191+
overriddenContracts.Count(i => i.Contains("IDirectConcreteContract", StringComparison.Ordinal)).ShouldBe(1, result);
192+
193+
foreach (var message in overriddenContracts)
194+
{
195+
message.ShouldNotContain("ConcreteBase");
196+
message.ShouldNotContain("IInheritedFromAbstractBase");
197+
message.ShouldNotContain("IInheritedFromConcreteBase");
198+
message.ShouldNotContain("IInheritedFromDirectInterface");
199+
message.ShouldNotContain("ICustomSpecialContract");
200+
message.ShouldNotContain("IDisposable");
201+
message.ShouldNotContain("IAsyncDisposable");
202+
message.ShouldNotContain("IEnumerable");
203+
}
204+
}
205+
}

‎tests/Pure.DI.IntegrationTests/TestExtensions.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ internal static Task<Result> RunAsync(this IEnumerable<string> sourceTexts, Opti
119119
.WithOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication).WithNullableContextOptions(runOptions.NullableContextOptions))
120120
.AddSyntaxTrees(generatedApiSources.Select(api => CSharpSyntaxTree.ParseText(api.SourceText, parseOptions)))
121121
.AddSyntaxTrees(sourceList.Select(source => CSharpSyntaxTree.ParseText(source, parseOptions)));
122+
if (!runOptions.MetadataReferences.IsDefaultOrEmpty)
123+
{
124+
compilation = compilation.AddReferences(runOptions.MetadataReferences);
125+
}
122126
// .Check(stdOut, options);
123127

124128
var globalOptions = new TestAnalyzerConfigOptions(new Dictionary<string, string>

0 commit comments

Comments
 (0)