Skip to content

Commit 9cbcb29

Browse files
authored
Add support for nullable analysis in interpolated string handler constructors (#57780)
Fixes #54583. We do not flow attribute post conditions back to the original slot of passed arguments or receivers, as without a proper in-order visit of the method parameters ensuring that the correct nullabilities are observed at the correct times isn't possible.
1 parent b246a00 commit 9cbcb29

16 files changed

+1444
-158
lines changed

src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2798,12 +2798,7 @@ internal static uint GetValEscape(BoundExpression expr, uint scopeOfTheContainin
27982798

27992799
if (conversion.ConversionKind == ConversionKind.InterpolatedStringHandler)
28002800
{
2801-
var data = conversion.Operand switch
2802-
{
2803-
BoundInterpolatedString { InterpolationData: { } d } => d,
2804-
BoundBinaryOperator { InterpolatedStringHandlerData: { } d } => d,
2805-
_ => throw ExceptionUtilities.UnexpectedValue(conversion.Operand.Kind)
2806-
};
2801+
var data = conversion.Operand.GetInterpolatedStringHandlerData();
28072802
return GetInterpolatedStringHandlerConversionEscapeScope(data, scopeOfTheContainingExpression);
28082803
}
28092804

@@ -3592,12 +3587,7 @@ private static bool CheckValEscape(ImmutableArray<BoundExpression> expressions,
35923587
private static bool CheckInterpolatedStringHandlerConversionEscape(BoundExpression expression, uint escapeFrom, uint escapeTo, BindingDiagnosticBag diagnostics)
35933588
{
35943589

3595-
var data = expression switch
3596-
{
3597-
BoundInterpolatedString { InterpolationData: { } d } => d,
3598-
BoundBinaryOperator { InterpolatedStringHandlerData: { } d } => d,
3599-
_ => throw ExceptionUtilities.UnexpectedValue(expression.Kind)
3600-
};
3590+
var data = expression.GetInterpolatedStringHandlerData();
36013591

36023592
// We need to check to see if any values could potentially escape outside the max depth via the handler type.
36033593
// Consider the case where a ref-struct handler saves off the result of one call to AppendFormatted,

src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2993,8 +2993,7 @@ private void CoerceArguments<TMember>(
29932993
MemberResolutionResult<TMember> methodResult,
29942994
ArrayBuilder<BoundExpression> arguments,
29952995
BindingDiagnosticBag diagnostics,
2996-
TypeSymbol? receiverType,
2997-
uint receiverEscapeScope)
2996+
BoundExpression? receiver)
29982997
where TMember : Symbol
29992998
{
30002999
var result = methodResult.Result;
@@ -3012,7 +3011,7 @@ private void CoerceArguments<TMember>(
30123011
Debug.Assert(argument is BoundUnconvertedInterpolatedString or BoundBinaryOperator { IsUnconvertedInterpolatedStringAddition: true });
30133012
TypeWithAnnotations parameterTypeWithAnnotations = GetCorrespondingParameterTypeWithAnnotations(ref result, parameters, arg);
30143013
reportUnsafeIfNeeded(methodResult, diagnostics, argument, parameterTypeWithAnnotations);
3015-
arguments[arg] = BindInterpolatedStringHandlerInMemberCall(argument, arguments, parameters, ref result, arg, receiverType, receiverEscapeScope, diagnostics);
3014+
arguments[arg] = BindInterpolatedStringHandlerInMemberCall(argument, arguments, parameters, ref result, arg, receiver, methodResult.LeastOverriddenMember.RequiresInstanceReceiver(), diagnostics);
30163015
}
30173016
// https://github.com/dotnet/roslyn/issues/37119 : should we create an (Identity) conversion when the kind is Identity but the types differ?
30183017
else if (!kind.IsIdentity)
@@ -4809,13 +4808,7 @@ private BoundExpression BindObjectInitializerMember(
48094808
{
48104809
if (argument is BoundConversion { Conversion.IsInterpolatedStringHandler: true, Operand: var operand })
48114810
{
4812-
var handlerPlaceholders = operand switch
4813-
{
4814-
BoundBinaryOperator { InterpolatedStringHandlerData: { } data } => data.ArgumentPlaceholders,
4815-
BoundInterpolatedString { InterpolationData: { } data } => data.ArgumentPlaceholders,
4816-
_ => throw ExceptionUtilities.UnexpectedValue(operand.Kind)
4817-
};
4818-
4811+
var handlerPlaceholders = operand.GetInterpolatedStringHandlerData().ArgumentPlaceholders;
48194812
if (handlerPlaceholders.Any(placeholder => placeholder.ArgumentIndex == BoundInterpolatedStringArgumentPlaceholder.InstanceParameter))
48204813
{
48214814
diagnostics.Add(ErrorCode.ERR_InterpolatedStringsReferencingInstanceCannotBeInObjectInitializers, argument.Syntax.Location);
@@ -5745,7 +5738,7 @@ internal bool TryPerformConstructorOverloadResolution(
57455738

57465739
if (succeededIgnoringAccessibility)
57475740
{
5748-
this.CoerceArguments<MethodSymbol>(result.ValidResult, analyzedArguments.Arguments, diagnostics, receiverType: null, receiverEscapeScope: Binder.ExternalScope);
5741+
this.CoerceArguments<MethodSymbol>(result.ValidResult, analyzedArguments.Arguments, diagnostics, receiver: null);
57495742
}
57505743

57515744
// Fill in the out parameter with the result, if there was one; it might be inaccessible.
@@ -8012,11 +8005,6 @@ private BoundExpression BindIndexerOrIndexedPropertyAccess(
80128005
{
80138006
MemberResolutionResult<PropertySymbol> resolutionResult = overloadResolutionResult.ValidResult;
80148007
PropertySymbol property = resolutionResult.Member;
8015-
RefKind? receiverRefKind = receiver.GetRefKind();
8016-
uint receiverEscapeScope = property.RequiresInstanceReceiver && receiver != null
8017-
? receiverRefKind?.IsWritableReference() == true ? GetRefEscape(receiver, LocalScopeDepth) : GetValEscape(receiver, LocalScopeDepth)
8018-
: Binder.ExternalScope;
8019-
this.CoerceArguments<PropertySymbol>(resolutionResult, analyzedArguments.Arguments, diagnostics, receiver.Type, receiverEscapeScope);
80208008

80218009
var isExpanded = resolutionResult.Result.Kind == MemberResolutionKind.ApplicableInExpandedForm;
80228010
var argsToParams = resolutionResult.Result.ArgsToParamsOpt;
@@ -8028,6 +8016,8 @@ private BoundExpression BindIndexerOrIndexedPropertyAccess(
80288016

80298017
receiver = ReplaceTypeOrValueReceiver(receiver, property.IsStatic, diagnostics);
80308018

8019+
this.CoerceArguments<PropertySymbol>(resolutionResult, analyzedArguments.Arguments, diagnostics, receiver);
8020+
80318021
if (!gotError && receiver != null && receiver.Kind == BoundKind.ThisReference && receiver.WasCompilerGenerated)
80328022
{
80338023
gotError = IsRefOrOutThisParameterCaptured(syntax, diagnostics);

src/Compilers/CSharp/Portable/Binder/Binder_InterpolatedString.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -808,8 +808,8 @@ private BoundExpression BindInterpolatedStringHandlerInMemberCall(
808808
ImmutableArray<ParameterSymbol> parameters,
809809
ref MemberAnalysisResult memberAnalysisResult,
810810
int interpolatedStringArgNum,
811-
TypeSymbol? receiverType,
812-
uint receiverEscapeScope,
811+
BoundExpression? receiver,
812+
bool requiresInstanceReceiver,
813813
BindingDiagnosticBag diagnostics)
814814
{
815815
Debug.Assert(unconvertedString is BoundUnconvertedInterpolatedString or BoundBinaryOperator { IsUnconvertedInterpolatedStringAddition: true });
@@ -916,9 +916,9 @@ private BoundExpression BindInterpolatedStringHandlerInMemberCall(
916916
switch (argumentIndex)
917917
{
918918
case BoundInterpolatedStringArgumentPlaceholder.InstanceParameter:
919-
Debug.Assert(receiverType is not null);
919+
Debug.Assert(receiver!.Type is not null);
920920
refKind = RefKind.None;
921-
placeholderType = receiverType;
921+
placeholderType = receiver.Type;
922922
break;
923923
case BoundInterpolatedStringArgumentPlaceholder.UnspecifiedParameter:
924924
{
@@ -964,33 +964,40 @@ private BoundExpression BindInterpolatedStringHandlerInMemberCall(
964964

965965
SyntaxNode placeholderSyntax;
966966
uint valSafeToEscapeScope;
967+
bool isSuppressed;
967968

968969
switch (argumentIndex)
969970
{
970971
case BoundInterpolatedStringArgumentPlaceholder.InstanceParameter:
971-
placeholderSyntax = unconvertedString.Syntax;
972-
valSafeToEscapeScope = receiverEscapeScope;
972+
Debug.Assert(receiver != null);
973+
valSafeToEscapeScope = requiresInstanceReceiver
974+
? receiver.GetRefKind().IsWritableReference() == true ? GetRefEscape(receiver, LocalScopeDepth) : GetValEscape(receiver, LocalScopeDepth)
975+
: Binder.ExternalScope;
976+
isSuppressed = receiver.IsSuppressed;
977+
placeholderSyntax = receiver.Syntax;
973978
break;
974979
case BoundInterpolatedStringArgumentPlaceholder.UnspecifiedParameter:
975980
placeholderSyntax = unconvertedString.Syntax;
976981
valSafeToEscapeScope = Binder.ExternalScope;
982+
isSuppressed = false;
977983
break;
978984
case >= 0:
979985
placeholderSyntax = arguments[argumentIndex].Syntax;
980986
valSafeToEscapeScope = GetValEscape(arguments[argumentIndex], LocalScopeDepth);
987+
isSuppressed = arguments[argumentIndex].IsSuppressed;
981988
break;
982989
default:
983990
throw ExceptionUtilities.UnexpectedValue(argumentIndex);
984991
}
985992

986993
argumentPlaceholdersBuilder.Add(
987-
new BoundInterpolatedStringArgumentPlaceholder(
994+
(BoundInterpolatedStringArgumentPlaceholder)(new BoundInterpolatedStringArgumentPlaceholder(
988995
placeholderSyntax,
989996
argumentIndex,
990997
valSafeToEscapeScope,
991998
placeholderType,
992999
hasErrors: argumentIndex == BoundInterpolatedStringArgumentPlaceholder.UnspecifiedParameter)
993-
{ WasCompilerGenerated = true });
1000+
{ WasCompilerGenerated = true }.WithSuppression(isSuppressed)));
9941001
// We use the parameter refkind, rather than what the argument was actually passed with, because that will suppress duplicated errors
9951002
// about arguments being passed with the wrong RefKind. The user will have already gotten an error about mismatched RefKinds or it will
9961003
// be a place where refkinds are allowed to differ

src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,11 +1022,7 @@ private BoundCall BindInvocationExpressionContinued(
10221022

10231023
var receiver = ReplaceTypeOrValueReceiver(methodGroup.Receiver, !method.RequiresInstanceReceiver && !invokedAsExtensionMethod, diagnostics);
10241024

1025-
var receiverRefKind = receiver?.GetRefKind();
1026-
uint receiverValEscapeScope = method.RequiresInstanceReceiver && receiver != null
1027-
? receiverRefKind?.IsWritableReference() == true ? GetRefEscape(receiver, LocalScopeDepth) : GetValEscape(receiver, LocalScopeDepth)
1028-
: Binder.ExternalScope;
1029-
this.CoerceArguments(methodResult, analyzedArguments.Arguments, diagnostics, receiver?.Type, receiverValEscapeScope);
1025+
this.CoerceArguments(methodResult, analyzedArguments.Arguments, diagnostics, receiver);
10301026

10311027
var expanded = methodResult.Result.Kind == MemberResolutionKind.ApplicableInExpandedForm;
10321028
var argsToParams = methodResult.Result.ArgsToParamsOpt;
@@ -2029,12 +2025,7 @@ private BoundFunctionPointerInvocation BindFunctionPointerInvocation(SyntaxNode
20292025
methodsBuilder.Free();
20302026

20312027
MemberResolutionResult<FunctionPointerMethodSymbol> methodResult = overloadResolutionResult.ValidResult;
2032-
CoerceArguments(
2033-
methodResult,
2034-
analyzedArguments.Arguments,
2035-
diagnostics,
2036-
receiverType: null,
2037-
receiverEscapeScope: Binder.ExternalScope);
2028+
CoerceArguments(methodResult, analyzedArguments.Arguments, diagnostics, receiver: null);
20382029

20392030
var args = analyzedArguments.Arguments.ToImmutable();
20402031
var refKinds = analyzedArguments.RefKinds.ToImmutableOrNull();

src/Compilers/CSharp/Portable/BoundTree/BoundNodeExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,5 +245,15 @@ static void pushLeftNodes(BoundBinaryOperator binary, ArrayBuilder<BoundBinaryOp
245245
}
246246
}
247247
}
248+
249+
public static InterpolatedStringHandlerData GetInterpolatedStringHandlerData(this BoundExpression e, bool throwOnMissing = true)
250+
=> e switch
251+
{
252+
BoundBinaryOperator { InterpolatedStringHandlerData: { } d } => d,
253+
BoundInterpolatedString { InterpolationData: { } d } => d,
254+
BoundBinaryOperator or BoundInterpolatedString when !throwOnMissing => default,
255+
BoundBinaryOperator or BoundInterpolatedString => throw ExceptionUtilities.Unreachable,
256+
_ => throw ExceptionUtilities.UnexpectedValue(e.Kind),
257+
};
248258
}
249259
}

src/Compilers/CSharp/Portable/BoundTree/InterpolatedStringHandlerData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ internal readonly struct InterpolatedStringHandlerData
2929

3030
public readonly BoundInterpolatedStringHandlerPlaceholder ReceiverPlaceholder;
3131

32+
public bool IsDefault => Construction is null;
33+
3234
public InterpolatedStringHandlerData(
3335
TypeSymbol builderType,
3436
BoundExpression construction,

src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,7 +1114,7 @@ public override BoundNode VisitDynamicInvocation(BoundDynamicInvocation node)
11141114
{ } d => (d.Construction, d.UsesBoolReturns, d.HasTrailingHandlerValidityParameter)
11151115
};
11161116

1117-
VisitRvalue(construction);
1117+
VisitInterpolatedStringHandlerConstructor(construction);
11181118
bool hasConditionalEvaluation = useBoolReturns || firstPartIsConditional;
11191119
TLocalState? shortCircuitState = hasConditionalEvaluation ? State.Clone() : default;
11201120

@@ -1128,6 +1128,11 @@ public override BoundNode VisitDynamicInvocation(BoundDynamicInvocation node)
11281128

11291129
return null;
11301130
}
1131+
1132+
protected virtual void VisitInterpolatedStringHandlerConstructor(BoundExpression? constructor)
1133+
{
1134+
VisitRvalue(constructor);
1135+
}
11311136
#nullable disable
11321137

11331138
public override BoundNode VisitInterpolatedString(BoundInterpolatedString node)
@@ -2459,7 +2464,7 @@ protected void VisitBinaryInterpolatedStringAddition(BoundBinaryOperator node)
24592464

24602465
Debug.Assert(parts.Count >= 2);
24612466

2462-
VisitRvalue(data.Construction);
2467+
VisitInterpolatedStringHandlerConstructor(data.Construction);
24632468

24642469
bool visitedFirst = false;
24652470
bool hasTrailingHandlerValidityParameter = data.HasTrailingHandlerValidityParameter;

src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.DebugVerifier.cs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,6 @@ private void VerifyExpression(BoundExpression expression, bool overrideSkippedEx
169169

170170
public override BoundNode? VisitBinaryOperator(BoundBinaryOperator node)
171171
{
172-
if (node.InterpolatedStringHandlerData is { } data)
173-
{
174-
Visit(data.Construction);
175-
}
176-
177172
VisitBinaryOperatorChildren(node);
178173
return null;
179174
}
@@ -253,23 +248,23 @@ private void VisitBinaryOperatorChildren(BoundBinaryOperatorBase node)
253248
return null;
254249
}
255250

256-
public override BoundNode? VisitInterpolatedString(BoundInterpolatedString node)
257-
{
258-
if (node.InterpolationData is { Construction: var construction })
259-
{
260-
Visit(construction);
261-
}
262-
base.VisitInterpolatedString(node);
263-
return null;
264-
}
265-
266251
public override BoundNode? VisitImplicitIndexerAccess(BoundImplicitIndexerAccess node)
267252
{
268253
Visit(node.Receiver);
269254
Visit(node.Argument);
270255
Visit(node.IndexerOrSliceAccess);
271256
return null;
272257
}
258+
259+
public override BoundNode? VisitConversion(BoundConversion node)
260+
{
261+
if (node.ConversionKind == ConversionKind.InterpolatedStringHandler)
262+
{
263+
Visit(node.Operand.GetInterpolatedStringHandlerData().Construction);
264+
}
265+
266+
return base.VisitConversion(node);
267+
}
273268
}
274269
#endif
275270
}

0 commit comments

Comments
 (0)