Skip to content

Commit 8cb8921

Browse files
adamhathcockNikolay Pianikov
authored andcommitted
Root functions should allow nullable args
1 parent 783d4b4 commit 8cb8921

6 files changed

Lines changed: 152 additions & 8 deletions

File tree

src/Pure.DI.Core/Core/ApiInvocationProcessor.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1087,7 +1087,17 @@ private void VisitArg(
10871087
if (genericName.TypeArgumentList.Arguments is [{} argTypeSyntax]
10881088
&& invocation.ArgumentList.Arguments is [{ Expression: {} nameArgExpression }, ..] args)
10891089
{
1090-
var argType = semantic.GetTypeSymbol<INamedTypeSymbol>(semanticModel, argTypeSyntax);
1090+
var argTypeInfo = semanticModel.GetTypeInfo(argTypeSyntax);
1091+
if ((argTypeInfo.Type ?? argTypeInfo.ConvertedType) is not INamedTypeSymbol argType)
1092+
{
1093+
argType = semantic.GetTypeSymbol<INamedTypeSymbol>(semanticModel, argTypeSyntax);
1094+
}
1095+
1096+
if (argTypeSyntax is NullableTypeSyntax && argType.IsReferenceType)
1097+
{
1098+
argType = (INamedTypeSymbol)argType.WithNullableAnnotation(NullableAnnotation.Annotated);
1099+
}
1100+
10911101
var tags = BuildTags(semanticModel, args.Skip(1));
10921102
var name = GetName(
10931103
nameArgExpression,

src/Pure.DI.Core/Core/Code/BuildTools.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ private string OnInjectedInternal(CodeContext ctx, VarInjection varInjection)
142142
}
143143
}
144144

145+
if (varInjection.Var.InstanceType.IsReferenceType
146+
&& varInjection.Var.InstanceType.NullableAnnotation == NullableAnnotation.Annotated
147+
&& varInjection.ContractType.NullableAnnotation != NullableAnnotation.Annotated)
148+
{
149+
variableCode = $"{variableCode}!";
150+
}
151+
145152
if (!ctx.RootContext.Graph.Source.Hints.IsOnDependencyInjectionEnabled)
146153
{
147154
return variableCode;
@@ -176,4 +183,4 @@ private string OnInjectedInternal(CodeContext ctx, VarInjection varInjection)
176183

177184
return tag;
178185
}
179-
}
186+
}

src/Pure.DI.Core/Core/Code/Parts/ParameterizedConstructorBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public CompositionCode Build(CompositionCode composition)
4040
foreach (var arg in classArgs)
4141
{
4242
var nullCheck = "";
43-
if (arg.InstanceType.IsReferenceType)
43+
if (arg.InstanceType.IsReferenceType && arg.InstanceType.NullableAnnotation != NullableAnnotation.Annotated)
4444
{
4545
nullCheck = $" ?? throw new {Names.SystemNamespace}ArgumentNullException(nameof({arg.Node.Arg?.Source.ArgName}))";
4646
}
@@ -55,7 +55,7 @@ public CompositionCode Build(CompositionCode composition)
5555
foreach (var arg in setupContextArgs)
5656
{
5757
var nullCheck = "";
58-
if (arg.Type.IsReferenceType)
58+
if (arg.Type.IsReferenceType && arg.Type.NullableAnnotation != NullableAnnotation.Annotated)
5959
{
6060
nullCheck = $" ?? throw new {Names.SystemNamespace}ArgumentNullException(nameof({arg.Name}))";
6161
}

src/Pure.DI.Core/Core/Code/Parts/RootMethodsBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ private void BuildRoot(CompositionCode composition, Root root)
120120
var indentToken = Disposables.Empty;
121121
if (root.IsMethod)
122122
{
123-
foreach (var arg in root.RootArgs.Where(i => i.InstanceType.IsReferenceType))
123+
foreach (var arg in root.RootArgs.Where(i => i.InstanceType.IsReferenceType && i.InstanceType.NullableAnnotation != NullableAnnotation.Annotated))
124124
{
125125
code.AppendLine($"if ({buildTools.NullCheck(composition.Compilation, arg.Name)}) throw new {Names.SystemNamespace}ArgumentNullException(nameof({arg.Name}));");
126126
}

src/Pure.DI.Core/Core/Code/TypeResolver.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private TypeDescription Resolve(MdSetup setup, ITypeSymbol type, ITypeParameterS
3636
}
3737
else
3838
{
39-
description = new TypeDescription(symbolNames.GetGlobalName(type), ImmutableArray<TypeDescription>.Empty, typeParam);
39+
description = new TypeDescription(GetGlobalName(type), ImmutableArray<TypeDescription>.Empty, typeParam);
4040
}
4141

4242
break;
@@ -55,6 +55,17 @@ private TypeDescription Resolve(MdSetup setup, ITypeSymbol type, ITypeParameterS
5555
}
5656
break;
5757

58+
case INamedTypeSymbol
59+
{
60+
ConstructedFrom.SpecialType: Microsoft.CodeAnalysis.SpecialType.System_Nullable_T,
61+
TypeArguments: [{} nullableValueType]
62+
}:
63+
{
64+
var nullableValueTypeDescription = Resolve(setup, nullableValueType);
65+
description = nullableValueTypeDescription with { Name = $"{nullableValueTypeDescription.Name}?" };
66+
}
67+
break;
68+
5869
case INamedTypeSymbol namedTypeSymbol:
5970
{
6071
var typeArgs = new List<string>();
@@ -66,7 +77,8 @@ private TypeDescription Resolve(MdSetup setup, ITypeSymbol type, ITypeParameterS
6677
}
6778

6879
var name = string.Join("", namedTypeSymbol.ToDisplayParts().TakeWhile(i => i.ToString() != "<"));
69-
description = new TypeDescription($"{name}<{string.Join(", ", typeArgs)}>", args.Distinct().ToList(), typeParam);
80+
var nullableSuffix = namedTypeSymbol is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated } ? "?" : "";
81+
description = new TypeDescription($"{name}<{string.Join(", ", typeArgs)}>{nullableSuffix}", args.Distinct().ToList(), typeParam);
7082
}
7183
break;
7284

@@ -76,10 +88,14 @@ private TypeDescription Resolve(MdSetup setup, ITypeSymbol type, ITypeParameterS
7688
break;
7789

7890
default:
79-
description = new TypeDescription(symbolNames.GetGlobalName(type), ImmutableArray<TypeDescription>.Empty, typeParam);
91+
description = new TypeDescription(GetGlobalName(type), ImmutableArray<TypeDescription>.Empty, typeParam);
8092
break;
8193
}
8294

8395
return description;
8496
}
97+
98+
private string GetGlobalName(ITypeSymbol type) => type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated }
99+
? $"{symbolNames.GetGlobalName(type)}?"
100+
: symbolNames.GetGlobalName(type);
85101
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
$v=true
3+
$p=5
4+
$d=Root arguments
5+
$h=Use root arguments when you need to pass state into a specific root. Define them with `RootArg<T>(string argName)` (optionally with tags) and use them like any other dependency. A root that uses at least one root argument becomes a method, and only arguments used in that root's object graph appear in the method signature. Use unique argument names to avoid collisions.
6+
$h=Root arguments are useful when runtime values belong to one entry point, not to the whole composition.
7+
$h=>[!NOTE]
8+
$h=>Actually, root arguments work like normal bindings. The difference is that they bind to the values of the arguments. These values will be injected wherever they are required.
9+
$h=
10+
$f=When using root arguments, compilation warnings are emitted if `Resolve` methods are generated because these methods cannot create such roots. Disable `Resolve` via `Hint(Hint.Resolve, "Off")`, or ignore the warnings and accept the risks.
11+
$r=Shouldly
12+
$f=Limitations: roots with root arguments become methods and are incompatible with generated `Resolve` methods.
13+
$f=Common pitfalls:
14+
$f=- Reusing ambiguous argument names for different concepts.
15+
$f=- Forgetting to disable or avoid `Resolve` usage in these setups.
16+
$f=See also: [Composition arguments](composition-arguments.md), [Resolve hint](resolve-hint.md).
17+
*/
18+
19+
// ReSharper disable ClassNeverInstantiated.Local
20+
// ReSharper disable CheckNamespace
21+
// ReSharper disable UnusedParameter.Local
22+
// ReSharper disable ArrangeTypeModifiers
23+
24+
namespace Pure.DI.UsageTests.Basics.RootArgumentsScenarioNullable;
25+
26+
using Shouldly;
27+
using Xunit;
28+
using static Tag;
29+
30+
// {
31+
//# using Pure.DI;
32+
//# using static Pure.DI.Tag;
33+
// }
34+
35+
public class ScenarioNullable
36+
{
37+
[Fact]
38+
public void Run()
39+
{
40+
// Root arguments make Resolve unusable, so disable Resolve generation
41+
// Resolve = Off
42+
// {
43+
DI.Setup(nameof(Composition))
44+
// Disable Resolve methods because root arguments are not compatible
45+
.Hint(Hint.Resolve, "Off")
46+
.Bind<IDatabaseServiceNullable>().To<DatabaseServiceNullable>()
47+
.Bind<IApplicationNullable>().To<ApplicationNullable>()
48+
49+
// Root arguments serve as values passed
50+
// to the composition root method
51+
.RootArg<int?>("port")
52+
.RootArg<string?>("connectionString")
53+
54+
// An argument can be tagged
55+
// to be injectable by type and this tag
56+
.RootArg<string>("appName", AppDetail)
57+
58+
// Composition root
59+
.Root<IApplicationNullable>("CreateApplication");
60+
61+
var composition = new Composition();
62+
63+
string? connectionString = null;
64+
int? port = 8080;
65+
66+
// Creates an application with specific arguments
67+
var app = composition.CreateApplication(
68+
appName: "MySuperApp",
69+
port: port,
70+
connectionString: connectionString);
71+
72+
app.Name.ShouldBe("MySuperApp");
73+
app.Database.Port.ShouldBe(8080);
74+
app.Database.ConnectionString.ShouldBeNull();
75+
// }
76+
composition.SaveClassDiagram();
77+
}
78+
}
79+
80+
// {
81+
interface IDatabaseServiceNullable
82+
{
83+
int? Port { get; }
84+
85+
string? ConnectionString { get; }
86+
}
87+
88+
class DatabaseServiceNullable(int? port, string? connectionString) : IDatabaseServiceNullable
89+
{
90+
public int? Port { get; } = port;
91+
92+
public string? ConnectionString { get; } = connectionString;
93+
}
94+
95+
interface IApplicationNullable
96+
{
97+
string Name { get; }
98+
99+
IDatabaseServiceNullable Database { get; }
100+
}
101+
102+
class ApplicationNullable(
103+
[Tag(AppDetail)] string name,
104+
IDatabaseServiceNullable database)
105+
: IApplicationNullable
106+
{
107+
public string Name { get; } = name;
108+
109+
public IDatabaseServiceNullable Database { get; } = database;
110+
}
111+
// }

0 commit comments

Comments
 (0)