Skip to content

Commit

Permalink
feat: support nullable parameters (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
skarllot committed Nov 22, 2023
1 parent 53ac7bc commit 0059d44
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 2 deletions.
17 changes: 17 additions & 0 deletions src/Jab.FunctionalTests.Common/ConstructorSelectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public void PassesOptionalParametersWhenAvailable()
Assert.NotNull(service.Parameter1);
Assert.Null(service.Parameter2);
Assert.NotNull(service.Parameter3);
Assert.False(typeof(IServiceProvider<IService2>).IsAssignableFrom(typeof(PassesOptionalParametersWhenAvailableContainer)));
}

[ServiceProvider]
Expand All @@ -83,5 +84,21 @@ public void IgnoresNonReferenceTypedParameters()
[Transient(typeof(IService3), typeof(ServiceImplementation))]
[Transient(typeof(IService), typeof(ServiceImplementationWithParameter<IService1, int, IService3>))]
internal partial class IgnoresNonReferenceTypedParametersContainer { }

[Fact]
public void IgnoresNullableOptionalParametersWhenNotAvailable()
{
IgnoresNullableOptionalParametersWhenNotAvailableContainer c = new();
var service = Assert.IsType<ServiceImplementationWithNullableOptional>(c.GetService<IService>());
Assert.NotNull(service.Parameter1);
Assert.Null(service.Parameter2);
Assert.Empty(service.Parameter3!);
Assert.False(typeof(IServiceProvider<IService2>).IsAssignableFrom(typeof(IgnoresNullableOptionalParametersWhenNotAvailableContainer)));
}

[ServiceProvider]
[Transient(typeof(IService1), typeof(ServiceImplementation))]
[Transient(typeof(IService), typeof(ServiceImplementationWithNullableOptional))]
internal partial class IgnoresNullableOptionalParametersWhenNotAvailableContainer { }
}
}
Original file line number Diff line number Diff line change
@@ -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<IService3>? Parameter3 { get; }

public ServiceImplementationWithNullableOptional(
IService1 parameter1,
IService2? parameter2 = null,
IEnumerable<IService3>? parameter3 = null)
{
Parameter1 = parameter1;
Parameter2 = parameter2;
Parameter3 = parameter3;
}
}
41 changes: 41 additions & 0 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
using Xunit;
using Verify = JabTests.GeneratorAnalyzerVerifier<Jab.ContainerGenerator>;
Expand Down Expand Up @@ -279,5 +280,45 @@ public partial class Container {{}}
.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.Error)
.WithLocation(1)
.WithArguments("IDependency?", "Service"));
}

[Fact]
public async Task ProducesJAB0014WhenNullableNonOptionalDependencyFound()
{
string testCode = $@"
#nullable enable
interface IDependency {{ }}
class Dependency : IDependency {{ }}
class Service {{ public Service(IDependency? dep) {{}} }}
[ServiceProvider]
[{{|#1:Transient(typeof(Service))|}}]
[{{|#2:Transient(typeof(IDependency), typeof(Dependency))|}}]
public partial class Container {{}}
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0014")
.WithSeverity(DiagnosticSeverity.Warning)
.WithLocation(1)
.WithArguments("IDependency?", "Service"));
}
}
}
2 changes: 1 addition & 1 deletion src/Jab/CodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ private void AppendType(INamedTypeSymbol namedTypeSymbol)
{
if (!_typeNameCache.TryGetValue(namedTypeSymbol, out var name))
{
name = _typeNameCache[namedTypeSymbol] = namedTypeSymbol.ToDisplayString();
name = _typeNameCache[namedTypeSymbol] = namedTypeSymbol.ToDisplayString(NullableFlowState.NotNull);
}

AppendRaw(name);
Expand Down
2 changes: 2 additions & 0 deletions src/Jab/ContainerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,8 @@ public override void Initialize(AnalysisContext context)
DiagnosticDescriptors.NoServiceTypeRegistered,
DiagnosticDescriptors.ImplementationTypeAndFactoryNotAllowed,
DiagnosticDescriptors.FactoryMemberMustBeAMethodOrHaveDelegateType,
DiagnosticDescriptors.NullableServiceNotRegistered,
DiagnosticDescriptors.NullableServiceRegistered,
}.ToImmutableArray();

private static string ReadAttributesFile()
Expand Down
8 changes: 8 additions & 0 deletions src/Jab/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,12 @@ 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",
"Not registered nullable dependency without a default value",
"The nullable service '{0}' requested to construct '{1}' is not registered. Add a default value to make the service reference optional", "Usage", DiagnosticSeverity.Error, true);

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.Warning, true);

}
15 changes: 14 additions & 1 deletion src/Jab/ServiceProviderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -690,9 +690,11 @@ ImmutableArray<IParameterSymbol> GetDelegateParameters(ITypeSymbol type)
}
else
{
bool isNullable = parameterSymbol.Type.NullableAnnotation == NullableAnnotation.Annotated;
if (parameterCallSite == null)
{
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.ServiceRequiredToConstructNotRegistered,
var diagnostic = Diagnostic.Create(
isNullable ? DiagnosticDescriptors.NullableServiceNotRegistered : DiagnosticDescriptors.ServiceRequiredToConstructNotRegistered,
registrationLocation,
parameterSymbol.Type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
implementationType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
Expand All @@ -702,6 +704,17 @@ ImmutableArray<IParameterSymbol> GetDelegateParameters(ITypeSymbol type)
}
else
{
if (isNullable)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.NullableServiceRegistered,
registrationLocation,
parameterSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
implementationType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));

_context.ReportDiagnostic(diagnostic);
}

callSites.Add(parameterCallSite);
}
}
Expand Down

0 comments on commit 0059d44

Please sign in to comment.