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
70 changes: 70 additions & 0 deletions src/Jab.Tests/DiagnosticsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,76 @@ await Verify.VerifyAnalyzerAsync(testCode,
.WithArguments("Dependency", "Named", "Service"));
}

[Fact]
public async Task DoesNotProduceDiagnosticForResolveDelegateWhenServiceRegistered()
{
string testCode = @"
interface IService { }
class Service : IService { }
class Consumer { public Consumer(Resolve<IService> resolver) { } }

[ServiceProvider]
[Transient(typeof(IService), typeof(Service))]
[Transient(typeof(Consumer))]
public partial class Container { }
";
await Verify.VerifyAnalyzerAsync(testCode);
}

[Fact]
public async Task ProducesDiagnosticWhenResolveDelegateServiceNotRegistered()
{
string testCode = @"
interface IService { }
class Consumer { public Consumer({|#1:Resolve<IService>|} resolver) { } }

[ServiceProvider]
[Transient(typeof(Consumer))]
public partial class Container { }
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0021")
.WithLocation(1)
.WithArguments("IService"));
}

[Fact]
public async Task ProducesDiagnosticWhenNamedResolveDelegateHasNoNamedService()
{
string testCode = @"
interface IService { }
class Service : IService { }
class Consumer { public Consumer({|#1:NamedResolve<IService>|} resolver) { } }

[ServiceProvider]
[Transient(typeof(IService), typeof(Service))]
[Transient(typeof(Consumer))]
public partial class Container { }
";
await Verify.VerifyAnalyzerAsync(testCode,
DiagnosticResult
.CompilerError("JAB0022")
.WithLocation(1)
.WithArguments("IService"));
}

[Fact]
public async Task DoesNotProduceDiagnosticForNamedResolveWhenNamedServiceRegistered()
{
string testCode = @"
interface IService { }
class Service : IService { }
class Consumer { public Consumer(NamedResolve<IService> resolver) { } }

[ServiceProvider]
[Singleton(typeof(IService), typeof(Service), Name = "Named")]
[Transient(typeof(Consumer))]
public partial class Container { }
";
await Verify.VerifyAnalyzerAsync(testCode);
}

[Fact]
public async Task ProducesJAB0002WhenRequiredDependenciesNotFound()
{
Expand Down
6 changes: 5 additions & 1 deletion src/Jab/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@

### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|------
JAB0021 | Usage | Error | Resolve delegate requires registered service
JAB0022 | Usage | Error | NamedResolve delegate requires named service registration
16 changes: 16 additions & 0 deletions src/Jab/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,22 @@ interface INamedServiceProvider<T>
T GetService(string name);
}

#if JAB_ATTRIBUTES_PACKAGE
public
#else
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Jab", null)]
internal
#endif
delegate T Resolve<T>(string name);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make Resolve delegate parameterless

Resolve<T> is declared with a string name parameter, identical to NamedResolve<T>, even though the generator emits the delegate for unnamed services and never forwards a name (see the lambda _ => GetService<T>()). As a result, consumers must supply a dummy string argument (resolver("ignored")) and cannot use the natural parameterless invocation (resolver()), and any name they pass is silently ignored. This makes the new API unusable for its intended lazy resolution scenario and will fail to compile in typical usage.

Useful? React with 👍 / 👎.


#if JAB_ATTRIBUTES_PACKAGE
public
#else
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Jab", null)]
internal
#endif
delegate T NamedResolve<T>(string name);

#if JAB_ATTRIBUTES_PACKAGE
public
#else
Expand Down
13 changes: 13 additions & 0 deletions src/Jab/ContainerGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,19 @@ private void GenerateCallSite(
}
});
break;
case ResolveDelegateCallSite resolveDelegateCallSite:
valueCallback(codeWriter, w =>
{
if (resolveDelegateCallSite.UsesName)
{
w.Append($"new global::Jab.NamedResolve<{resolveDelegateCallSite.ResolvedType}>(GetService<{resolveDelegateCallSite.ResolvedType}>)");
}
else
{
w.Append($"new global::Jab.Resolve<{resolveDelegateCallSite.ResolvedType}>(_ => GetService<{resolveDelegateCallSite.ResolvedType}>())");
}
});
break;
case ServiceProviderCallSite:
valueCallback(codeWriter, w => w.AppendRaw("this"));
break;
Expand Down
8 changes: 8 additions & 0 deletions src/Jab/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ internal static class DiagnosticDescriptors
"Only string service keys are supported",
"Service key '{0}' is not supported, only string keys are supported", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor ResolveDelegateServiceNotRegistered = new("JAB0021",
"Resolve delegate requires registered service",
"Resolve delegate requires the service '{0}' to be registered", "Usage", DiagnosticSeverity.Error, true);

public static readonly DiagnosticDescriptor NamedResolveDelegateServiceNotRegistered = new("JAB0022",
"NamedResolve delegate requires named service registration",
"NamedResolve delegate requires the service '{0}' to be registered with a name", "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);
Expand Down
10 changes: 10 additions & 0 deletions src/Jab/KnownTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ internal class KnownTypes
public const string ServiceProviderModuleAttributeShortName = "ServiceProviderModule";
public const string ImportAttributeShortName = "Import";
public const string FromNamedServicesAttributeShortName = "FromNamedServices";
public const string ResolveDelegateShortName = "Resolve";
public const string NamedResolveDelegateShortName = "NamedResolve";

public const string TransientAttributeTypeName = $"{TransientAttributeShortName}Attribute";
public const string SingletonAttributeTypeName = $"{SingletonAttributeShortName}Attribute";
Expand All @@ -19,6 +21,8 @@ internal class KnownTypes

public const string ImportAttributeTypeName = $"{ImportAttributeShortName}Attribute";
public const string FromNamedServicesAttributeName = $"{FromNamedServicesAttributeShortName}Attribute";
public const string ResolveDelegateTypeName = ResolveDelegateShortName;
public const string NamedResolveDelegateTypeName = NamedResolveDelegateShortName;

public const string TransientAttributeMetadataName = $"Jab.{TransientAttributeTypeName}";
public const string GenericTransientAttributeMetadataName = $"Jab.{TransientAttributeTypeName}`1";
Expand Down Expand Up @@ -51,6 +55,8 @@ internal class KnownTypes
private const string IKeyedServiceProviderMetadataName = "Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider";
private const string FromKeyedServicesAttributeMetadataName = "Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute";
private const string FromNamedServicesAttributeMetadataName = $"Jab.{FromNamedServicesAttributeName}";
private const string ResolveDelegateMetadataName = $"Jab.{ResolveDelegateTypeName}`1";
private const string NamedResolveDelegateMetadataName = $"Jab.{NamedResolveDelegateTypeName}`1";

private const string IServiceScopeFactoryMetadataName =
"Microsoft.Extensions.DependencyInjection.IServiceScopeFactory";
Expand Down Expand Up @@ -83,6 +89,8 @@ internal class KnownTypes
public INamedTypeSymbol? IKeyedServiceProviderType { get; }
public INamedTypeSymbol? FromKeyedServicesAttribute { get; }
public INamedTypeSymbol? FromNamedServicesAttribute { get; }
public INamedTypeSymbol ResolveDelegateType { get; }
public INamedTypeSymbol NamedResolveDelegateType { get; }

public KnownTypes(Compilation compilation, IModuleSymbol module, IAssemblySymbol assemblySymbol)
{
Expand Down Expand Up @@ -130,6 +138,8 @@ static INamedTypeSymbol GetTypeFromCompilationByMetadataNameOrThrow(Compilation

ModuleAttribute = GetTypeByMetadataNameOrThrow(assemblySymbol, ServiceProviderModuleAttributeMetadataName);
FromNamedServicesAttribute = GetTypeByMetadataNameOrThrow(assemblySymbol, FromNamedServicesAttributeMetadataName);
ResolveDelegateType = GetTypeByMetadataNameOrThrow(assemblySymbol, ResolveDelegateMetadataName);
NamedResolveDelegateType = GetTypeByMetadataNameOrThrow(assemblySymbol, NamedResolveDelegateMetadataName);
}

public static bool HasKnownTypes(IModuleSymbol sourceModule)
Expand Down
14 changes: 14 additions & 0 deletions src/Jab/ResolveDelegateCallSite.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Jab;

internal record ResolveDelegateCallSite : ServiceCallSite
{
public ResolveDelegateCallSite(ServiceIdentity identity, ITypeSymbol resolvedType, bool usesName)
: base(identity, identity.Type, ServiceLifetime.Transient, false)
{
ResolvedType = resolvedType;
UsesName = usesName;
}

public ITypeSymbol ResolvedType { get; }
public bool UsesName { get; }
}
104 changes: 104 additions & 0 deletions src/Jab/ServiceProviderBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,31 @@ ServiceCallSite BuiltInCallSite(ServiceCallSite callSite)
return callSite;
}

if (serviceType is INamedTypeSymbol { IsGenericType: true } delegateType)
{
if (SymbolEqualityComparer.Default.Equals(delegateType.ConstructedFrom, _knownTypes.ResolveDelegateType))
{
var identity = new ServiceIdentity(serviceType, name, null);
if (CheckNotNamed(identity) is { } error)
{
return error;
}

return CreateResolveDelegateCallSite(identity, delegateType.TypeArguments[0], requiresNamed: false, context);
}

if (SymbolEqualityComparer.Default.Equals(delegateType.ConstructedFrom, _knownTypes.NamedResolveDelegateType))
{
var identity = new ServiceIdentity(serviceType, name, null);
if (CheckNotNamed(identity) is { } error)
{
return error;
}

return CreateResolveDelegateCallSite(identity, delegateType.TypeArguments[0], requiresNamed: true, context);
}
}

if (SymbolEqualityComparer.Default.Equals(serviceType, _knownTypes.IServiceProviderType))
{
return BuiltInCallSite(_serviceProviderCallsite);
Expand Down Expand Up @@ -723,6 +748,47 @@ private ServiceCallSite CreateConstructorCallSite(
return (callSites, namedParameters, diagnostics);
}

private ServiceCallSite? CreateResolveDelegateCallSite(ServiceIdentity identity, ITypeSymbol resolvedType, bool requiresNamed, ServiceResolutionContext context)
{
if (context.CallSiteCache.TryGet(identity, out var existing))
{
return existing;
}

Diagnostic? diagnostic = null;

if (requiresNamed)
{
if (!HasNamedRegistration(resolvedType, context.ProviderDescription))
{
diagnostic = Diagnostic.Create(
DiagnosticDescriptors.NamedResolveDelegateServiceNotRegistered,
context.RequestLocation,
resolvedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
}
}
else
{
if (!CanSatisfy(resolvedType, context.ProviderDescription))
{
diagnostic = Diagnostic.Create(
DiagnosticDescriptors.ResolveDelegateServiceNotRegistered,
context.RequestLocation,
resolvedType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
}
}

if (diagnostic != null)
{
_context.ReportDiagnostic(diagnostic);
return new ErrorCallSite(identity, diagnostic);
}

var callSite = new ResolveDelegateCallSite(identity, resolvedType, requiresNamed);
context.CallSiteCache.Add(callSite);
return callSite;
}

private bool CanSatisfy(ITypeSymbol serviceType, ServiceProviderDescription description)
{
INamedTypeSymbol? genericType = null;
Expand All @@ -739,6 +805,19 @@ private bool CanSatisfy(ITypeSymbol serviceType, ServiceProviderDescription desc
return true;
}

if (genericType != null)
{
if (SymbolEqualityComparer.Default.Equals(genericType.ConstructedFrom, _knownTypes.ResolveDelegateType))
{
return CanSatisfy(genericType.TypeArguments[0], description);
}

if (SymbolEqualityComparer.Default.Equals(genericType.ConstructedFrom, _knownTypes.NamedResolveDelegateType))
{
return HasNamedRegistration(genericType.TypeArguments[0], description);
}
}

foreach (var registration in description.ServiceRegistrations)
{
if (SymbolEqualityComparer.Default.Equals(registration.ServiceType.ConstructedFrom, serviceType))
Expand All @@ -758,6 +837,31 @@ private bool CanSatisfy(ITypeSymbol serviceType, ServiceProviderDescription desc
return false;
}

private bool HasNamedRegistration(ITypeSymbol serviceType, ServiceProviderDescription description)
{
foreach (var registration in description.ServiceRegistrations)
{
if (registration.Name == null)
{
continue;
}

if (SymbolEqualityComparer.Default.Equals(registration.ServiceType, serviceType))
{
return true;
}

if (serviceType is INamedTypeSymbol { IsGenericType: true } genericServiceType &&
registration.ServiceType.IsUnboundGenericType &&
SymbolEqualityComparer.Default.Equals(registration.ServiceType.ConstructedFrom, genericServiceType.ConstructedFrom))
{
return true;
}
}

return false;
}

private IMethodSymbol? SelectConstructor(INamedTypeSymbol implementationType,
ServiceProviderDescription description)
{
Expand Down