From 6087551c1364318cdfc88e5fbc2db50d007f6330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabr=C3=ADcio=20Godoy?= Date: Mon, 20 Nov 2023 13:43:02 -0300 Subject: [PATCH] feat: support nullable as optional dependency --- .../ConstructorSelectionTests.cs | 33 +++++++++++++++++++ .../Jab.FunctionalTest.props | 1 + .../ServiceImplementationWithNullable.cs | 22 +++++++++++++ ...rviceImplementationWithNullableOptional.cs | 22 +++++++++++++ src/Jab.Tests/DiagnosticsTest.cs | 20 +++++++++++ src/Jab/ContainerGenerator.cs | 21 +++++++----- src/Jab/DefaultValueCallSite.cs | 10 ++++++ src/Jab/DiagnosticDescriptors.cs | 4 +++ src/Jab/ServiceCallSite.cs | 1 + src/Jab/ServiceProviderBuilder.cs | 13 +++++++- 10 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullable.cs create mode 100644 src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullableOptional.cs create mode 100644 src/Jab/DefaultValueCallSite.cs diff --git a/src/Jab.FunctionalTests.Common/ConstructorSelectionTests.cs b/src/Jab.FunctionalTests.Common/ConstructorSelectionTests.cs index 01ccb7c..2dd49d1 100644 --- a/src/Jab.FunctionalTests.Common/ConstructorSelectionTests.cs +++ b/src/Jab.FunctionalTests.Common/ConstructorSelectionTests.cs @@ -59,6 +59,7 @@ public void PassesOptionalParametersWhenAvailable() Assert.NotNull(service.Parameter1); Assert.Null(service.Parameter2); Assert.NotNull(service.Parameter3); + Assert.False(typeof(IServiceProvider).IsAssignableFrom(typeof(PassesOptionalParametersWhenAvailableContainer))); } [ServiceProvider] @@ -83,5 +84,37 @@ public void IgnoresNonReferenceTypedParameters() [Transient(typeof(IService3), typeof(ServiceImplementation))] [Transient(typeof(IService), typeof(ServiceImplementationWithParameter))] internal partial class IgnoresNonReferenceTypedParametersContainer { } + + [Fact] + public void IgnoresNullableParametersWhenNotAvailable() + { + IgnoresNullableParametersWhenNotAvailableContainer c = new(); + var service = Assert.IsType(c.GetService()); + Assert.NotNull(service.Parameter1); + Assert.Null(service.Parameter2); + Assert.Empty(service.Parameter3!); + Assert.False(typeof(IServiceProvider).IsAssignableFrom(typeof(IgnoresNullableOptionalParametersWhenNotAvailableContainer))); + } + + [ServiceProvider] + [Transient(typeof(IService1), typeof(ServiceImplementation))] + [Transient(typeof(IService), typeof(ServiceImplementationWithNullable))] + internal partial class IgnoresNullableParametersWhenNotAvailableContainer { } + + [Fact] + public void IgnoresNullableOptionalParametersWhenNotAvailable() + { + IgnoresNullableOptionalParametersWhenNotAvailableContainer c = new(); + var service = Assert.IsType(c.GetService()); + Assert.NotNull(service.Parameter1); + Assert.Null(service.Parameter2); + Assert.Empty(service.Parameter3!); + Assert.False(typeof(IServiceProvider).IsAssignableFrom(typeof(IgnoresNullableOptionalParametersWhenNotAvailableContainer))); + } + + [ServiceProvider] + [Transient(typeof(IService1), typeof(ServiceImplementation))] + [Transient(typeof(IService), typeof(ServiceImplementationWithNullableOptional))] + internal partial class IgnoresNullableOptionalParametersWhenNotAvailableContainer { } } } \ No newline at end of file diff --git a/src/Jab.FunctionalTests.Common/Jab.FunctionalTest.props b/src/Jab.FunctionalTests.Common/Jab.FunctionalTest.props index 2183623..8b6ebe8 100644 --- a/src/Jab.FunctionalTests.Common/Jab.FunctionalTest.props +++ b/src/Jab.FunctionalTests.Common/Jab.FunctionalTest.props @@ -10,6 +10,7 @@ preview JabTests false + $(NoWarn);JAB0013 diff --git a/src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullable.cs b/src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullable.cs new file mode 100644 index 0000000..156523d --- /dev/null +++ b/src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullable.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace JabTests; + +#nullable enable + +internal class ServiceImplementationWithNullable : IService +{ + public IService1 Parameter1 { get; } + public IService2? Parameter2 { get; } + public IEnumerable? Parameter3 { get; } + + public ServiceImplementationWithNullable( + IService1 parameter1, + IService2? parameter2, + IEnumerable? parameter3) + { + Parameter1 = parameter1; + Parameter2 = parameter2; + Parameter3 = parameter3; + } +} \ No newline at end of file diff --git a/src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullableOptional.cs b/src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullableOptional.cs new file mode 100644 index 0000000..0871c90 --- /dev/null +++ b/src/Jab.FunctionalTests.Common/Mocks/ServiceImplementationWithNullableOptional.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace JabTests; + +#nullable enable + +internal class ServiceImplementationWithNullableOptional : IService +{ + public IService1 Parameter1 { get; } + public IService2? Parameter2 { get; } + public IEnumerable? Parameter3 { get; } + + public ServiceImplementationWithNullableOptional( + IService1 parameter1, + IService2? parameter2 = null, + IEnumerable? parameter3 = null) + { + Parameter1 = parameter1; + Parameter2 = parameter2; + Parameter3 = parameter3; + } +} \ No newline at end of file diff --git a/src/Jab.Tests/DiagnosticsTest.cs b/src/Jab.Tests/DiagnosticsTest.cs index d9cf58b..75acdfc 100644 --- a/src/Jab.Tests/DiagnosticsTest.cs +++ b/src/Jab.Tests/DiagnosticsTest.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Testing; using Xunit; using Verify = JabTests.GeneratorAnalyzerVerifier; @@ -279,5 +280,24 @@ await Verify.VerifyAnalyzerAsync(testCode, .WithLocation(1) .WithArguments("IService")); } + + [Fact] + public async Task ProducesJAB0013WhenNullableNonOptionalDependencyNotFound() + { + string testCode = $@" +#nullable enable +interface IDependency {{ }} +class Service {{ public Service(IDependency? dep) {{}} }} +[ServiceProvider] +[{{|#1:Transient(typeof(Service))|}}] +public partial class Container {{}} +"; + await Verify.VerifyAnalyzerAsync(testCode, + DiagnosticResult + .CompilerError("JAB0013") + .WithSeverity(DiagnosticSeverity.Warning) + .WithLocation(1) + .WithArguments("IDependency?", "Service")); + } } } \ No newline at end of file diff --git a/src/Jab/ContainerGenerator.cs b/src/Jab/ContainerGenerator.cs index 4cfc715..4b87d15 100644 --- a/src/Jab/ContainerGenerator.cs +++ b/src/Jab/ContainerGenerator.cs @@ -54,9 +54,13 @@ private void GenerateCallSiteWithCache(CodeWriter codeWriter, string rootReferen private void WriteResolutionCall(CodeWriter codeWriter, ServiceCallSite other, string reference) { - if (other.IsMainImplementation) + if (other is DefaultValueCallSite) { - codeWriter.Append($"{reference}.GetService<{other.ServiceType}>()"); + codeWriter.Append($"default({other.ServiceTypeString})"); + } + else if (other.IsMainImplementation) + { + codeWriter.Append($"{reference}.GetService<{other.ServiceTypeString}>()"); } else { @@ -208,7 +212,7 @@ private void Execute(GeneratorContext context) foreach (var rootService in root.RootCallSites) { - var rootServiceType = rootService.ServiceType; + string rootServiceType = rootService.ServiceTypeString; if (rootService.IsMainImplementation) { codeWriter.Append($"{rootServiceType} IServiceProvider<{rootServiceType}>.GetService()"); @@ -267,7 +271,7 @@ private void Execute(GeneratorContext context) { codeWriter.Line($" ||"); } - codeWriter.Append($"typeof({rootService.ServiceType}) == service"); + codeWriter.Append($"typeof({rootService.ServiceType.ToDisplayString(NullableFlowState.NotNull)}) == service"); } if (first) { @@ -296,7 +300,7 @@ private void Execute(GeneratorContext context) foreach (var rootService in root.RootCallSites) { - var rootServiceType = rootService.ServiceType; + string rootServiceType = rootService.ServiceTypeString; using (rootService.IsMainImplementation ? codeWriter.Scope($"{rootServiceType} IServiceProvider<{rootServiceType}>.GetService()") : @@ -358,7 +362,7 @@ private void WriteServiceProvider(CodeWriter codeWriter, ServiceProvider root) { if (rootRootCallSite.IsMainImplementation) { - codeWriter.Append($"if (type == typeof({rootRootCallSite.ServiceType})) return "); + codeWriter.Append($"if (type == typeof({rootRootCallSite.ServiceType.ToDisplayString(NullableFlowState.NotNull)})) return "); WriteResolutionCall(codeWriter, rootRootCallSite, "this"); codeWriter.Line($";"); } @@ -494,7 +498,7 @@ private static void WriteInterfaces(CodeWriter codeWriter, ServiceProvider root, { if (serviceCallSite.IsMainImplementation) { - codeWriter.Line($" IServiceProvider<{serviceCallSite.ServiceType}>,"); + codeWriter.Line($" IServiceProvider<{serviceCallSite.ServiceTypeString}>,"); } } @@ -510,7 +514,7 @@ private void WriteCacheLocations(ServiceProvider root, CodeWriter codeWriter, bo (rootService.Lifetime == ServiceLifetime.Scoped && !isScope) || rootService.Lifetime == ServiceLifetime.Transient) continue; - codeWriter.Line($"private {rootService.ImplementationType}? {GetCacheLocation(rootService)};"); + codeWriter.Line($"private {rootService.ImplementationType.ToDisplayString(NullableFlowState.NotNull)}? {GetCacheLocation(rootService)};"); } codeWriter.Line(); } @@ -589,6 +593,7 @@ public override void Initialize(AnalysisContext context) DiagnosticDescriptors.NoServiceTypeRegistered, DiagnosticDescriptors.ImplementationTypeAndFactoryNotAllowed, DiagnosticDescriptors.FactoryMemberMustBeAMethodOrHaveDelegateType, + DiagnosticDescriptors.NullableServiceNotRegistered, }.ToImmutableArray(); private static string ReadAttributesFile() diff --git a/src/Jab/DefaultValueCallSite.cs b/src/Jab/DefaultValueCallSite.cs new file mode 100644 index 0000000..92f8e5b --- /dev/null +++ b/src/Jab/DefaultValueCallSite.cs @@ -0,0 +1,10 @@ +namespace Jab; + +internal record DefaultValueCallSite: ServiceCallSite +{ + public DefaultValueCallSite(ITypeSymbol serviceType) : base(serviceType, serviceType, ServiceLifetime.Transient, 0, false) + { + } + + public override string ServiceTypeString => ServiceType.ToDisplayString(NullableFlowState.MaybeNull); +} \ No newline at end of file diff --git a/src/Jab/DiagnosticDescriptors.cs b/src/Jab/DiagnosticDescriptors.cs index e14b060..c1eb5d6 100644 --- a/src/Jab/DiagnosticDescriptors.cs +++ b/src/Jab/DiagnosticDescriptors.cs @@ -50,4 +50,8 @@ internal static class DiagnosticDescriptors "The factory member has to be a method or have a delegate type", "The factory member '{0}' has to be a method of have a delegate type, for service '{1}'", "Usage", DiagnosticSeverity.Error, true); + public static readonly DiagnosticDescriptor NullableServiceNotRegistered = new("JAB0013", + "The requested non-optional nullable service is not registered", + "The non-optional nullable service '{0}' specified to construct '{1}' is not registered, define it as optional", "Usage", DiagnosticSeverity.Warning, true); + } diff --git a/src/Jab/ServiceCallSite.cs b/src/Jab/ServiceCallSite.cs index d5ce5db..662af7d 100644 --- a/src/Jab/ServiceCallSite.cs +++ b/src/Jab/ServiceCallSite.cs @@ -8,4 +8,5 @@ internal abstract record ServiceCallSite(ITypeSymbol ServiceType, ITypeSymbol Im public int ReverseIndex { get; } = ReverseIndex; public bool? IsDisposable { get; } = IsDisposable; public bool IsMainImplementation => ReverseIndex == 0; + public virtual string ServiceTypeString => ServiceType.ToDisplayString(NullableFlowState.NotNull); } \ No newline at end of file diff --git a/src/Jab/ServiceProviderBuilder.cs b/src/Jab/ServiceProviderBuilder.cs index 45fbf3c..3c4ed32 100644 --- a/src/Jab/ServiceProviderBuilder.cs +++ b/src/Jab/ServiceProviderBuilder.cs @@ -690,7 +690,18 @@ private ServiceCallSite CreateConstructorCallSite( } else { - if (parameterCallSite == null) + if (parameterCallSite == null && + parameterSymbol.Type.NullableAnnotation == NullableAnnotation.Annotated) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptors.NullableServiceNotRegistered, + registrationLocation, + parameterSymbol.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), + implementationType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + + _context.ReportDiagnostic(diagnostic); + callSites.Add(new DefaultValueCallSite(parameterSymbol.Type)); + } + else if (parameterCallSite == null) { var diagnostic = Diagnostic.Create(DiagnosticDescriptors.ServiceRequiredToConstructNotRegistered, registrationLocation,