Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@
<ItemGroup Condition="'$(FunctionalTestPackageVersion)' == ''" >
<ProjectReference Include="..\Jab.Attributes\Jab.Attributes.csproj" ReferenceOutputAssembly="true" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Jab.FunctionalTests.Common\Mocks\ServiceImplementationWithGenericServiceInConstructor.cs">
<Link>Mocks\ServiceImplementationWithGenericServiceInConstructor.cs</Link>
</Compile>
</ItemGroup>
</Project>
47 changes: 46 additions & 1 deletion src/Jab.FunctionalTests.Common/ContainerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,28 @@ public void CanResolveOpenGenericService()
Assert.IsType<AnotherServiceImplementation>(service.InnerService);
}

[ServiceProvider(RootServices = new[] { typeof(IService<IAnotherService>) })]
[ServiceProvider]
[Transient(typeof(IService<>), typeof(ServiceImplementation<>))]
[Transient(typeof(IAnotherService), typeof(AnotherServiceImplementation))]
internal partial class CanResolveOpenGenericServiceContainer { }

[Fact]
public void CanResolveOpenGenericServiceFromConstructor()
{
CanResolveOpenGenericServiceFromConstructorContainer c = new();
var service = c.GetService<IService>();
Assert.IsType<ServiceImplementationWithGenericServiceInConstructor>(service);
var genericService = ((ServiceImplementationWithGenericServiceInConstructor)service).GenericService;
Assert.IsType<ServiceImplementation<IAnotherService>>(genericService);
var innerService = genericService.InnerService;
Assert.IsType<AnotherServiceImplementation>(innerService);
}

[ServiceProvider]
[Transient(typeof(IService<>), typeof(ServiceImplementation<>))]
[Transient(typeof(IService), typeof(ServiceImplementationWithGenericServiceInConstructor))]
[Transient(typeof(IAnotherService), typeof(AnotherServiceImplementation))]
internal partial class CanResolveOpenGenericServiceFromConstructorContainer { }

[Fact]
public void CanResolveEnumerableOfMixedOpenGenericService()
Expand Down Expand Up @@ -1084,6 +1102,33 @@ partial class CanGetMultipleOpenGenericScopedContainer
{
}

[Fact]
public void CanResolveOpenGenericWithValueTypeArgument()
{
CanResolveOpenGenericWithValueTypeArgumentContainer c = new();
Assert.IsType<MessageBroker<int>>(c.GetService<IPublisher<int>>());
}

[ServiceProvider]
[Singleton(typeof(IPublisher<>), typeof(MessageBroker<>))]
partial class CanResolveOpenGenericWithValueTypeArgumentContainer
{
}

[Fact]
public void CanResolveOpenGenericUsingFactoryMethod()
{
CanResolveOpenGenericUsingFactoryMethodContainer c = new();
Assert.IsType<MessageBroker<string>>(c.GetService<IPublisher<string>>());
}

[ServiceProvider]
[Singleton(typeof(IPublisher<>), Factory = nameof(CreatePublisher))]
partial class CanResolveOpenGenericUsingFactoryMethodContainer
{
public IPublisher<T> CreatePublisher<T>() => new MessageBroker<T>();
}

#region Non-generic member factory with parameters
[Fact]
public void CanUseSingletonFactoryWithParameters()
Expand Down
9 changes: 9 additions & 0 deletions src/Jab.FunctionalTests.Common/Mocks/Publishing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace JabTests;

Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The IPublisher interface is empty and lacks documentation explaining its purpose as a test mock for open generic publisher pattern validation. Consider adding a comment or summary to clarify its role in the test suite.

Suggested change
/// <summary>
/// Test mock interface used for validating the open generic publisher pattern in the test suite.
/// </summary>

Copilot uses AI. Check for mistakes.
internal interface IPublisher<T>
{
}

internal class MessageBroker<T> : IPublisher<T>
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace JabTests;

internal class ServiceImplementationWithGenericServiceInConstructor : IService
{
public ServiceImplementationWithGenericServiceInConstructor(IService<IAnotherService> genericService)
{
GenericService = genericService;
}

public IService<IAnotherService> GenericService { get; }
}
185 changes: 183 additions & 2 deletions src/Jab.Tests/DiagnosticsTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Jab;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
Expand Down Expand Up @@ -424,7 +425,7 @@ await Verify.VerifyAnalyzerAsync(testCode,
.WithLocation(1)
.WithArguments("IDependency?", "Service"));
}

[Fact]
public async Task AllowsExistingGenericAttribute()
{
Expand All @@ -434,7 +435,7 @@ class Implementation : IService { }

[ServiceProvider]
[Singleton(typeof(Implementation))]
[Existing<IService, Implementation>]
[Existing(typeof(IService), typeof(Implementation))]
public partial class Container { }
";

Expand Down Expand Up @@ -479,5 +480,185 @@ await Verify.VerifyAnalyzerAsync(testCode,
.WithLocation(1)
.WithArguments("Implementation", "IService"));
}

[Fact]
public async Task ProducesJAB0023WhenOpenGenericRegistrationUsesInstance()
{
string testCode = @"
public interface IPublisher<T> { }
public class Publisher<T> : IPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), Instance = nameof(PublisherInstance))|}]
public partial class Container
{
public IPublisher<int> PublisherInstance => null;
}
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0023")
.WithLocation(1)
.WithArguments("IPublisher<>")
);
}

[Fact]
public async Task ProducesJAB0024WhenOpenGenericImplementationIsClosed()
{
string testCode = @"
public interface IPublisher<T> { }
public class ClosedPublisher : IPublisher<int> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), typeof(ClosedPublisher))|}]
public partial class Container { }
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0024")
.WithLocation(1)
.WithArguments("ClosedPublisher", "IPublisher<>")
);
}

[Fact]
public async Task ProducesJAB0025WhenOpenGenericImplementationArityMismatches()
{
string testCode = @"
public interface IPublisher<T> { }
public class Publisher<T, TOther> : IPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), typeof(Publisher<,>))|}]
public partial class Container { }
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0025")
.WithLocation(1)
.WithArguments("Publisher<,>", "IPublisher<>", 1)
);
}

[Fact]
public async Task ProducesJAB0026WhenOpenGenericImplementationNotAssignable()
{
string testCode = @"
public interface IPublisher<T> { }
public class NotPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), typeof(NotPublisher<>))|}]
public partial class Container { }
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0026")
.WithLocation(1)
.WithArguments("NotPublisher<>", "IPublisher<>")
);
}

[Fact]
public async Task ProducesJAB0026WhenOpenGenericImplementationFixesTypeArgument()
{
string testCode = @"
public interface IPublisher<T> { }
public class PublisherWithFixedType<T> : IPublisher<int> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), typeof(PublisherWithFixedType<>))|}]
public partial class Container { }
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0026")
.WithLocation(1)
.WithArguments("PublisherWithFixedType<>", "IPublisher<>")
);
}

[Fact]
public async Task ProducesJAB0027WhenOpenGenericFactoryNotGenericMethod()
{
string testCode = @"
public interface IPublisher<T> { }
public class MessageBroker<T> : IPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), Factory = nameof(PublisherFactory))|}]
public partial class Container
{
public IPublisher<int> PublisherFactory => null;
}
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0027")
.WithLocation(1)
.WithArguments("PublisherFactory", "IPublisher<>", 1)
);
}

[Fact]
public async Task ProducesJAB0028WhenOpenGenericFactoryReturnsWrongType()
{
string testCode = @"
public interface IPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), Factory = nameof(CreatePublisher))|}]
public partial class Container
{
public object CreatePublisher<T>() => null;
}
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0028")
.WithLocation(1)
.WithArguments("CreatePublisher", "object", "IPublisher<>")
);
}

[Fact]
public async Task ProducesJAB0028WhenOpenGenericFactoryFixesTypeArgument()
{
string testCode = @"
public interface IPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>), Factory = nameof(CreatePublisher))|}]
public partial class Container
{
public IPublisher<int> CreatePublisher<T>() => null;
}
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0028")
.WithLocation(1)
.WithArguments("CreatePublisher", "IPublisher<int>", "IPublisher<>")
);
}

[Fact]
public async Task ProducesJAB0029WhenOpenGenericInterfaceHasNoImplementation()
{
string testCode = @"
public interface IPublisher<T> { }
[ServiceProvider]
[{|#1:Singleton(typeof(IPublisher<>))|}]
public partial class Container { }
";

await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0029")
.WithLocation(1)
.WithArguments("IPublisher<>")
);
}
}
}
7 changes: 7 additions & 0 deletions src/Jab/ContainerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,13 @@ public override void Initialize(AnalysisContext context)
DiagnosticDescriptors.OnlyStringKeysAreSupported,
DiagnosticDescriptors.NullableServiceNotRegistered,
DiagnosticDescriptors.NullableServiceRegistered,
DiagnosticDescriptors.OpenGenericInstanceNotSupported,
DiagnosticDescriptors.OpenGenericImplementationMustBeOpenGeneric,
DiagnosticDescriptors.OpenGenericImplementationArityMismatch,
DiagnosticDescriptors.OpenGenericImplementationNotAssignable,
DiagnosticDescriptors.OpenGenericFactoryMustBeGenericMethod,
DiagnosticDescriptors.OpenGenericFactoryReturnTypeNotAssignable,
DiagnosticDescriptors.OpenGenericServiceRequiresImplementation,
DiagnosticDescriptors.ExistingImplementationMustImplementService,
DiagnosticDescriptors.ExistingImplementationTypeNotRegistered,
}.ToImmutableArray();
Expand Down
28 changes: 28 additions & 0 deletions src/Jab/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,32 @@ internal static class DiagnosticDescriptors
public static readonly DiagnosticDescriptor NullableServiceRegistered = new("JAB0014",
"Nullable dependency without a default value",
"'{0}' parameter to construct '{1}' will never be null when constructing using a service provider. Add a default value to make the service reference optional", "Usage", DiagnosticSeverity.Info, true);

public static readonly DiagnosticDescriptor OpenGenericInstanceNotSupported = new("JAB0023",
"Open generic registrations cannot use instances",
"Open generic service '{0}' cannot be registered using an instance", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor OpenGenericImplementationMustBeOpenGeneric = new("JAB0024",
"Open generic implementations must be open generic definitions",
"The implementation type '{0}' for open generic service '{1}' must be an open generic type definition", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor OpenGenericImplementationArityMismatch = new("JAB0025",
"Open generic implementations must have matching arity",
"The implementation type '{0}' for open generic service '{1}' must declare exactly {2} type parameter(s)", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor OpenGenericImplementationNotAssignable = new("JAB0026",
"Open generic implementation must be assignable",
"The implementation type '{0}' for open generic service '{1}' must be assignable to the service", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor OpenGenericFactoryMustBeGenericMethod = new("JAB0027",
"Open generic factories must be generic methods",
"The factory member '{0}' for open generic service '{1}' must be a generic method with {2} type parameter(s)", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor OpenGenericFactoryReturnTypeNotAssignable = new("JAB0028",
"Open generic factory return type must be assignable",
"The factory method '{0}' must return a type assignable to open generic service '{2}', but returns '{1}'", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor OpenGenericServiceRequiresImplementation = new("JAB0029",
"Open generic service requires an implementation",
"Open generic service '{0}' must specify an implementation type or factory because it cannot be instantiated directly", "Usage", DiagnosticSeverity.Error, true);
}
Loading