-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Collection expressions: nullable analysis of spread element expression #74686
Changes from 10 commits
b14f4da
a874fd1
7e67677
71877b6
4b1a2ad
caee2a2
2a21d6e
3d5f15f
343a43c
92aaed3
df8a770
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3664,8 +3664,20 @@ protected override void VisitStatement(BoundStatement statement) | |
VisitRvalue(initializer.Arguments[0]); | ||
break; | ||
case BoundCollectionExpressionSpreadElement spread: | ||
// https://github.com/dotnet/roslyn/issues/68786: We should check the spread | ||
Visit(spread); | ||
if (elementType.HasType && | ||
spread.ElementPlaceholder is { } elementPlaceholder && | ||
spread.IteratorBody is { }) | ||
{ | ||
var itemResult = spread.EnumeratorInfoOpt == null ? default : _visitResult; | ||
var iteratorBody = ((BoundExpressionStatement)spread.IteratorBody).Expression; | ||
AddPlaceholderReplacement(elementPlaceholder, expression: elementPlaceholder, itemResult); | ||
var completion = VisitOptionalImplicitConversion(iteratorBody, elementType, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like we are assuming the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't find the connection between Also, it feels like using a name like That said, none of the above changes need to happen in this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. I've renamed |
||
useLegacyWarnings: false, trackMembers: false, AssignmentKind.Assignment, delayCompletionForTargetType: true).completion; | ||
Debug.Assert(completion is not null); | ||
elementConversionCompletions.Add(completion); | ||
RemovePlaceholderReplacement(elementPlaceholder); | ||
} | ||
break; | ||
default: | ||
var elementExpr = (BoundExpression)element; | ||
|
@@ -3767,6 +3779,34 @@ static NullableFlowState getResultState(BoundCollectionExpression node, Collecti | |
} | ||
} | ||
|
||
public override BoundNode? VisitCollectionExpressionSpreadElement(BoundCollectionExpressionSpreadElement node) | ||
{ | ||
VisitRvalue(node.Expression); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like a case like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the top-level nullability of the expression being spread is handled in |
||
|
||
if (node.Conversion is BoundConversion { Conversion: var conversion }) | ||
{ | ||
Debug.Assert(node.ExpressionPlaceholder is { }); | ||
Debug.Assert(node.EnumeratorInfoOpt is { }); | ||
AddPlaceholderReplacement(node.ExpressionPlaceholder, node.Expression, _visitResult); | ||
VisitForEachExpression( | ||
node, | ||
node.Conversion, | ||
conversion, | ||
node.ExpressionPlaceholder, | ||
node.EnumeratorInfoOpt, | ||
awaitOpt: null); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
RemovePlaceholderReplacement(node.ExpressionPlaceholder); | ||
} | ||
else | ||
{ | ||
Debug.Assert(node.HasErrors); | ||
Debug.Assert(node.Conversion is null); | ||
Debug.Assert(node.EnumeratorInfoOpt is null); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
private void VisitObjectCreationExpressionBase(BoundObjectCreationExpressionBase node) | ||
{ | ||
Debug.Assert(!IsConditionalState); | ||
|
@@ -10612,6 +10652,18 @@ private TypeWithAnnotations GetDeclaredParameterResult(ParameterSymbol parameter | |
return null; | ||
} | ||
|
||
public override BoundNode? VisitCollectionExpressionSpreadExpressionPlaceholder(BoundCollectionExpressionSpreadExpressionPlaceholder node) | ||
{ | ||
VisitPlaceholderWithReplacement(node); | ||
return null; | ||
} | ||
|
||
public override BoundNode? VisitValuePlaceholder(BoundValuePlaceholder node) | ||
{ | ||
VisitPlaceholderWithReplacement(node); | ||
return null; | ||
} | ||
|
||
public override BoundNode? VisitEventAccess(BoundEventAccess node) | ||
{ | ||
var updatedSymbol = VisitMemberAccess(node, node.ReceiverOpt, node.EventSymbol); | ||
|
@@ -10711,6 +10763,23 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
var (expr, conversion) = RemoveConversion(node.Expression, includeExplicitConversions: false); | ||
SnapshotWalkerThroughConversionGroup(node.Expression, expr); | ||
|
||
VisitForEachExpression( | ||
node, | ||
node.Expression, | ||
conversion, | ||
expr, | ||
node.EnumeratorInfoOpt, | ||
node.AwaitOpt); | ||
} | ||
|
||
private void VisitForEachExpression( | ||
BoundNode node, | ||
BoundExpression collectionExpression, | ||
Conversion conversion, | ||
BoundExpression expr, | ||
ForEachEnumeratorInfo? enumeratorInfoOpt, | ||
BoundAwaitableInfo? awaitOpt) | ||
{ | ||
// There are 7 ways that a foreach can be created: | ||
// 1. The collection type is an array type. For this, initial binding will generate an implicit reference conversion to | ||
// IEnumerable, and we do not need to do any reinferring of enumerators here. | ||
|
@@ -10744,7 +10813,7 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
|
||
MethodSymbol? reinferredGetEnumeratorMethod = null; | ||
|
||
if (node.EnumeratorInfoOpt?.GetEnumeratorInfo is { Method: { IsExtensionMethod: true, Parameters: var parameters } } enumeratorMethodInfo) | ||
if (enumeratorInfoOpt?.GetEnumeratorInfo is { Method: { IsExtensionMethod: true, Parameters: var parameters } } enumeratorMethodInfo) | ||
{ | ||
// this is case 7 | ||
// We do not need to do this same analysis for non-extension methods because they do not have generic parameters that | ||
|
@@ -10772,13 +10841,13 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
} | ||
else if (conversion.IsImplicit) | ||
{ | ||
bool isAsync = node.AwaitOpt != null; | ||
if (node.Expression.Type!.SpecialType == SpecialType.System_Collections_IEnumerable) | ||
bool isAsync = awaitOpt != null; | ||
if (collectionExpression.Type!.SpecialType == SpecialType.System_Collections_IEnumerable) | ||
{ | ||
// If this is a conversion to IEnumerable (non-generic), nothing to do. This is cases 1, 2, and 5. | ||
targetTypeWithAnnotations = TypeWithAnnotations.Create(node.Expression.Type); | ||
targetTypeWithAnnotations = TypeWithAnnotations.Create(collectionExpression.Type); | ||
} | ||
else if (ForEachLoopBinder.IsIEnumerableT(node.Expression.Type.OriginalDefinition, isAsync, compilation)) | ||
else if (ForEachLoopBinder.IsIEnumerableT(collectionExpression.Type.OriginalDefinition, isAsync, compilation)) | ||
{ | ||
// This is case 4. We need to look for the IEnumerable<T> that this reinferred expression implements, | ||
// so that we pick up any nested type substitutions that could have occurred. | ||
|
@@ -10801,7 +10870,7 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
} | ||
|
||
var convertedResult = VisitConversion( | ||
GetConversionIfApplicable(node.Expression, expr), | ||
GetConversionIfApplicable(collectionExpression, expr), | ||
expr, | ||
conversion, | ||
targetTypeWithAnnotations, | ||
|
@@ -10811,15 +10880,15 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
useLegacyWarnings: false, | ||
AssignmentKind.Assignment); | ||
|
||
bool reportedDiagnostic = node.EnumeratorInfoOpt?.GetEnumeratorInfo.Method is { IsExtensionMethod: true } | ||
bool reportedDiagnostic = enumeratorInfoOpt?.GetEnumeratorInfo.Method is { IsExtensionMethod: true } | ||
? false | ||
: CheckPossibleNullReceiver(expr); | ||
|
||
SetAnalyzedNullability(node.Expression, new VisitResult(convertedResult, convertedResult.ToTypeWithAnnotations(compilation))); | ||
SetAnalyzedNullability(collectionExpression, new VisitResult(convertedResult, convertedResult.ToTypeWithAnnotations(compilation))); | ||
|
||
TypeWithState currentPropertyGetterTypeWithState; | ||
|
||
if (node.EnumeratorInfoOpt is null) | ||
if (enumeratorInfoOpt is null) | ||
{ | ||
currentPropertyGetterTypeWithState = default; | ||
} | ||
|
@@ -10834,17 +10903,17 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
// There are frameworks where System.String does not implement IEnumerable, but we still lower it to a for loop | ||
// using the indexer over the individual characters anyway. So the type must be not annotated char. | ||
currentPropertyGetterTypeWithState = | ||
TypeWithAnnotations.Create(node.EnumeratorInfoOpt.ElementType, NullableAnnotation.NotAnnotated).ToTypeWithState(); | ||
TypeWithAnnotations.Create(enumeratorInfoOpt.ElementType, NullableAnnotation.NotAnnotated).ToTypeWithState(); | ||
} | ||
else | ||
{ | ||
// Reinfer the return type of the node.Expression.GetEnumerator().Current property, so that if | ||
// Reinfer the return type of the collectionExpression.GetEnumerator().Current property, so that if | ||
// the collection changed nested generic types we pick up those changes. | ||
if (reinferredGetEnumeratorMethod is null) | ||
{ | ||
TypeSymbol? getEnumeratorType; | ||
|
||
if (node.EnumeratorInfoOpt is { InlineArraySpanType: not WellKnownType.Unknown and var wellKnownSpan }) | ||
if (enumeratorInfoOpt is { InlineArraySpanType: not WellKnownType.Unknown and var wellKnownSpan }) | ||
{ | ||
Debug.Assert(wellKnownSpan is WellKnownType.System_Span_T or WellKnownType.System_ReadOnlySpan_T); | ||
NamedTypeSymbol spanType = compilation.GetWellKnownType(wellKnownSpan); | ||
|
@@ -10855,29 +10924,29 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
getEnumeratorType = convertedResult.Type; | ||
} | ||
|
||
reinferredGetEnumeratorMethod = (MethodSymbol)AsMemberOfType(getEnumeratorType, node.EnumeratorInfoOpt.GetEnumeratorInfo.Method); | ||
reinferredGetEnumeratorMethod = (MethodSymbol)AsMemberOfType(getEnumeratorType, enumeratorInfoOpt.GetEnumeratorInfo.Method); | ||
} | ||
|
||
var enumeratorReturnType = GetReturnTypeWithState(reinferredGetEnumeratorMethod); | ||
|
||
if (enumeratorReturnType.State != NullableFlowState.NotNull) | ||
{ | ||
if (!reportedDiagnostic && !(node.Expression is BoundConversion { Operand: { IsSuppressed: true } })) | ||
if (!reportedDiagnostic && !(collectionExpression is BoundConversion { Operand: { IsSuppressed: true } })) | ||
{ | ||
ReportDiagnostic(ErrorCode.WRN_NullReferenceReceiver, expr.Syntax.GetLocation()); | ||
} | ||
} | ||
|
||
var currentPropertyGetter = (MethodSymbol)AsMemberOfType(enumeratorReturnType.Type, node.EnumeratorInfoOpt.CurrentPropertyGetter); | ||
var currentPropertyGetter = (MethodSymbol)AsMemberOfType(enumeratorReturnType.Type, enumeratorInfoOpt.CurrentPropertyGetter); | ||
|
||
currentPropertyGetterTypeWithState = ApplyUnconditionalAnnotations( | ||
currentPropertyGetter.ReturnTypeWithAnnotations.ToTypeWithState(), | ||
currentPropertyGetter.ReturnTypeFlowAnalysisAnnotations); | ||
|
||
// Analyze `await MoveNextAsync()` | ||
if (node.AwaitOpt is { AwaitableInstancePlaceholder: BoundAwaitableValuePlaceholder moveNextPlaceholder } awaitMoveNextInfo) | ||
if (awaitOpt is { AwaitableInstancePlaceholder: BoundAwaitableValuePlaceholder moveNextPlaceholder } awaitMoveNextInfo) | ||
{ | ||
var moveNextAsyncMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, node.EnumeratorInfoOpt.MoveNextInfo.Method); | ||
var moveNextAsyncMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, enumeratorInfoOpt.MoveNextInfo.Method); | ||
|
||
var result = new VisitResult(GetReturnTypeWithState(moveNextAsyncMethod), moveNextAsyncMethod.ReturnTypeWithAnnotations); | ||
AddPlaceholderReplacement(moveNextPlaceholder, moveNextPlaceholder, result); | ||
|
@@ -10886,11 +10955,11 @@ protected override void VisitForEachExpression(BoundForEachStatement node) | |
} | ||
|
||
// Analyze `await DisposeAsync()` | ||
if (node.EnumeratorInfoOpt is { NeedsDisposal: true, DisposeAwaitableInfo: BoundAwaitableInfo awaitDisposalInfo }) | ||
if (enumeratorInfoOpt is { NeedsDisposal: true, DisposeAwaitableInfo: BoundAwaitableInfo awaitDisposalInfo }) | ||
{ | ||
var disposalPlaceholder = awaitDisposalInfo.AwaitableInstancePlaceholder; | ||
bool addedPlaceholder = false; | ||
if (node.EnumeratorInfoOpt.PatternDisposeInfo is { Method: var originalDisposeMethod }) // no statically known Dispose method if doing a runtime check | ||
if (enumeratorInfoOpt.PatternDisposeInfo is { Method: var originalDisposeMethod }) // no statically known Dispose method if doing a runtime check | ||
{ | ||
Debug.Assert(disposalPlaceholder is not null); | ||
var disposeAsyncMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, originalDisposeMethod); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we actually need to dig into the expression here, instead of just visiting the statement, for the verifier to pick it up? #Resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NullableWalker
only visits the expression within theBoundExpressionStatement
ofspread.IteratorBody
. (The containingBoundExpressionStatement
is only there to allow sharing code in binding betweenforeach
and..
since theforeach
infrastructure expects the body to be a statement.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, sounds like if we visited the statement here, then we would fail verification because NullableWalker did not also visit that statement.