Skip to content
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

Enforce unconditional attributes within method bodies #40789

Merged
merged 13 commits into from
Jan 18, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,11 @@ could be different than the one that compiler used to find.

16. In *Visual Studio 2019 version 16.5* and language version 8.0 and later, the compiler will no longer accept `throw null` when there is no type `System.Exception`.

17. https://github.com/dotnet/roslyn/issues/39852 Previously the compiler would allow an invocation of an implicit index or range indexer to specify any named argument. In *Visual Studio 2019 version 16.5* argument names are no longer permitted for these invocations.
17. https://github.com/dotnet/roslyn/issues/39852 Previously the compiler would allow an invocation of an implicit index or range indexer to specify any named argument. In *Visual Studio 2019 version 16.5* argument names are no longer permitted for these invocations.

18. https://github.com/dotnet/roslyn/issues/36039 In *Visual Studio 2019 version 16.3* and onwards, the compiler only honored nullability flow annotation attributes for callers of an API. In *Visual Studio 2019 version 16.5* the compiler honors those attributes within member bodies.
For instance, returning `default` from a method declared with a `[MaybeNull] T` return type will no longer warn.
Similarly, a value from a `[DisallowNull] ref string? p` parameter will be assumed to be not-null when first read.
On the other hand, we'll warn for returning a `null` from a method declared with `[NotNull] string?` return type, and we'll treat the value from a `[AllowNull] ref string p` parameter as maybe-null.
Conditional attributes are treated leniently. For instance, no warning will be produced for assigning a maybe-null value to an `[MaybeNullWhen(...)] out string p` parameter.

4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/CSharpResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/CSharpResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -5439,10 +5439,10 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ
<value>Argument cannot be used as an output for parameter due to differences in the nullability of reference types.</value>
</data>
<data name="WRN_DisallowNullAttributeForbidsMaybeNullAssignment" xml:space="preserve">
<value>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</value>
<value>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</value>
</data>
<data name="WRN_DisallowNullAttributeForbidsMaybeNullAssignment_Title" xml:space="preserve">
<value>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</value>
<value>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</value>
</data>
<data name="WRN_NullabilityMismatchInReturnTypeOfTargetDelegate" xml:space="preserve">
<value>Nullability of reference types in return type of '{0}' doesn't match the target delegate '{1}'.</value>
Expand Down
112 changes: 80 additions & 32 deletions src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1420,9 +1420,17 @@ private void EnterParameter(ParameterSymbol parameter, TypeWithAnnotations param

private static TypeWithState GetParameterState(TypeWithAnnotations parameterType, FlowAnalysisAnnotations parameterAnnotations)
{
return ((parameterAnnotations & FlowAnalysisAnnotations.AllowNull) != 0) ?
TypeWithState.Create(parameterType.Type, NullableFlowState.MaybeDefault) :
parameterType.ToTypeWithState();
if ((parameterAnnotations & FlowAnalysisAnnotations.AllowNull) != 0)
{
return TypeWithState.Create(parameterType.Type, NullableFlowState.MaybeDefault);
}

if ((parameterAnnotations & FlowAnalysisAnnotations.DisallowNull) != 0)
{
return TypeWithState.Create(parameterType.Type, NullableFlowState.NotNull);
}

return parameterType.ToTypeWithState();
}

protected override BoundNode VisitReturnStatementNoAdjust(BoundReturnStatement node)
Expand All @@ -1437,17 +1445,21 @@ protected override BoundNode VisitReturnStatementNoAdjust(BoundReturnStatement n

// Should not convert to method return type when inferring return type (when _returnTypesOpt != null).
if (_returnTypesOpt == null &&
TryGetReturnType(out TypeWithAnnotations returnType))
TryGetReturnType(out TypeWithAnnotations returnType, out FlowAnalysisAnnotations returnAnnotations))
{
TypeWithState returnState;
if (node.RefKind == RefKind.None)
{
VisitOptionalImplicitConversion(expr, returnType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Return);
returnState = VisitOptionalImplicitConversion(expr, returnType, useLegacyWarnings: false, trackMembers: false, AssignmentKind.Return);
}
else
{
// return ref expr;
VisitRefExpression(expr, returnType);
returnState = VisitRefExpression(expr, returnType);
}

// If the return has annotations, we perform an additional check for nullable value types
CheckDisallowedNullAssignment(returnState, ToInwardAnnotations(returnAnnotations), node.Syntax.Location, boundValueOpt: expr);
}
else
{
Expand Down Expand Up @@ -1483,12 +1495,13 @@ private TypeWithState VisitRefExpression(BoundExpression expr, TypeWithAnnotatio
return resultType;
}

private bool TryGetReturnType(out TypeWithAnnotations type)
private bool TryGetReturnType(out TypeWithAnnotations type, out FlowAnalysisAnnotations annotations)
{
var method = _symbol as MethodSymbol;
if (method is null)
{
type = default;
annotations = FlowAnalysisAnnotations.None;
return false;
}

Expand All @@ -1499,22 +1512,26 @@ private bool TryGetReturnType(out TypeWithAnnotations type)
if (returnType.IsVoidType())
{
type = default;
annotations = FlowAnalysisAnnotations.None;
return false;
}

if (!method.IsAsync)
{
type = ApplyUnconditionalAnnotations(returnType, delegateOrMethod.ReturnTypeFlowAnalysisAnnotations);
annotations = delegateOrMethod.ReturnTypeFlowAnalysisAnnotations;
type = ApplyUnconditionalAnnotations(returnType, annotations);
return true;
}

if (returnType.Type.IsGenericTaskType(compilation))
{
type = ((NamedTypeSymbol)returnType.Type).TypeArgumentsWithAnnotationsNoUseSiteDiagnostics.Single();
annotations = FlowAnalysisAnnotations.None;
return true;
}

type = default;
annotations = FlowAnalysisAnnotations.None;
return false;
}

Expand Down Expand Up @@ -3224,21 +3241,11 @@ private void ReinferMethodAndVisitArguments(BoundCall node, TypeWithState receiv
{
returnState = returnState.WithNotNullState();
}
ReportMaybeNullFromTypeParameterValueIfNeeded(node, returnState, GetRValueAnnotations(method));

SetResult(node, returnState, method.ReturnTypeWithAnnotations);
SetUpdatedSymbol(node, node.Method, method);
}

/// <summary>
/// Members that return [MaybeNull]T values (for an unconstrained type parameter) produce a warning upon usage,
/// just like default(T).
/// </summary>
private void ReportMaybeNullFromTypeParameterValueIfNeeded(BoundExpression expr, TypeWithState typeWithState, FlowAnalysisAnnotations annotations)
{
// Not implemented. See https://github.com/dotnet/roslyn/issues/38638 for an alternative.
}

private void LearnFromEqualsMethod(MethodSymbol method, BoundCall node, TypeWithState receiverType, ImmutableArray<VisitArgumentResult> results)
{
// easy out
Expand All @@ -3254,7 +3261,6 @@ private void LearnFromEqualsMethod(MethodSymbol method, BoundCall node, TypeWith
return;
}


var isStaticEqualsMethod = method.Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__EqualsObjectObject))
|| method.Equals(compilation.GetSpecialTypeMember(SpecialMember.System_Object__ReferenceEquals));
if (isStaticEqualsMethod ||
Expand Down Expand Up @@ -3498,6 +3504,11 @@ private static TypeWithAnnotations ApplyUnconditionalAnnotations(TypeWithAnnotat
return declaredType.AsAnnotated();
}

if ((annotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull)
{
return declaredType.AsNotAnnotated();
}

return declaredType;
}

Expand Down Expand Up @@ -3883,12 +3894,40 @@ private void VisitArgumentConversionAndInboundAssignmentsAndPreConditions(
Debug.Assert(!this.IsConditionalState);
}

private void CheckDisallowedNullAssignment(TypeWithState state, FlowAnalysisAnnotations annotations, Location location)
private void CheckDisallowedNullAssignment(TypeWithState state, FlowAnalysisAnnotations annotations, Location location, BoundExpression boundValueOpt = null)
Copy link
Contributor

@RikkiGibson RikkiGibson Jan 17, 2020

Choose a reason for hiding this comment

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

Consider naming CheckDisallowNullAssignment to more strongly denote that this is about DisallowNullAttribute #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that "check disallowed null assignment" rings better as an action. I'll keep as-is


In reply to: 368161189 [](ancestors = 368161189)

{
if (((annotations & FlowAnalysisAnnotations.DisallowNull) != 0) && state.Type.IsNullableTypeOrTypeParameter() && state.MayBeNull)
if (boundValueOpt is { WasCompilerGenerated: true })
{
// We need to skip `return backingField;` in auto-prop getters
Copy link
Member

@cston cston Jan 16, 2020

Choose a reason for hiding this comment

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

// We need to skip return backingField; in auto-prop getters [](start = 16, length = 62)

Can we simply avoid running NullableWalker on auto-property accessors? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

We could, but I'd prefer to contain the scope of this PR given timeline.
Also, even if we skip auto-prop accessors, we'll still need some logic to skip compiler-generated nodes because of field = initializerValue; that is injected to constructor body and analyzed.


In reply to: 367228167 [](ancestors = 367228167)

Copy link
Member

Choose a reason for hiding this comment

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

Please consider logging a bug to skip auto-property accessors.


In reply to: 367632535 [](ancestors = 367632535,367228167)

Copy link
Member Author

@jcouv jcouv Jan 16, 2020

Choose a reason for hiding this comment

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

Filed #41017 #Resolved

return;
}

// We do this extra check for types whose non-nullable version cannot be represented
if (((annotations & FlowAnalysisAnnotations.DisallowNull) != 0) && hasNoNonNullableCounterpart(state.Type) && state.MayBeNull)
{
ReportDiagnostic(ErrorCode.WRN_DisallowNullAttributeForbidsMaybeNullAssignment, location);
}

static bool hasNoNonNullableCounterpart(TypeSymbol type)
{
if (type is null)
Copy link
Member Author

@jcouv jcouv Jan 15, 2020

Choose a reason for hiding this comment

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

📝 not sure if we can hit this. Just included for safety. #Resolved

{
return false;
}

// Some types that could receive a maybe-null value have a NotNull counterpart:
// [NotNull]string? -> string
// [NotNull]string -> string
// [NotNull]TClass -> TClass
// [NotNull]TClass? -> TClass
//
// While others don't:
// [NotNull]int? -> X
// [NotNull]TNullable -> X
// [NotNull]TStruct? -> X
// [NotNull]TOpen -> X
return (type.Kind == SymbolKind.TypeParameter && !type.IsReferenceType) || type.IsNullableTypeOrTypeParameter();
}
}

/// <summary>
Expand Down Expand Up @@ -3930,8 +3969,6 @@ private void VisitArgumentOutboundAssignmentsAndPostConditions(
{
ReportNullableAssignmentIfNecessary(parameterValue, lValueType, applyPostConditionsUnconditionally(parameterWithState, parameterAnnotations), UseLegacyWarnings(argument, result.LValueType));
}

ReportMaybeNullFromTypeParameterValueIfNeeded(argument, parameterWithState, parameterAnnotations);
}
break;
case RefKind.Out:
Expand Down Expand Up @@ -3977,8 +4014,6 @@ private void VisitArgumentOutboundAssignmentsAndPostConditions(
ReportNullabilityMismatchInArgument(argument.Syntax, lValueType.Type, parameter, parameterType.Type, forOutput: true);
}
}

ReportMaybeNullFromTypeParameterValueIfNeeded(argument, parameterWithState, parameterAnnotations);
}
break;
default:
Expand Down Expand Up @@ -6058,6 +6093,7 @@ private FlowAnalysisAnnotations GetLValueAnnotations(BoundExpression expr)
BoundFieldAccess field => getFieldAnnotations(field.FieldSymbol),
BoundObjectInitializerMember { MemberSymbol: PropertySymbol prop } => getSetterAnnotations(prop),
BoundObjectInitializerMember { MemberSymbol: FieldSymbol field } => getFieldAnnotations(field),
BoundParameter { ParameterSymbol: ParameterSymbol parameter } => ToInwardAnnotations(GetParameterAnnotations(parameter)),
_ => FlowAnalysisAnnotations.None
};

Expand Down Expand Up @@ -6099,6 +6135,22 @@ static FlowAnalysisAnnotations getPropertyAnnotations(SourcePropertySymbol prope
}
}

private static FlowAnalysisAnnotations ToInwardAnnotations(FlowAnalysisAnnotations outwardAnnotations)
Copy link
Contributor

Choose a reason for hiding this comment

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

ToInwardAnnotations [](start = 47, length = 19)

Consider naming ToInboundAnnotations to resemble similar terminology used in VisitArguments

{
var annotations = FlowAnalysisAnnotations.None;
if ((outwardAnnotations & FlowAnalysisAnnotations.MaybeNull) != 0)
{
// MaybeNull and MaybeNullWhen count as MaybeNull
annotations |= FlowAnalysisAnnotations.AllowNull;
}
if ((outwardAnnotations & FlowAnalysisAnnotations.NotNull) == FlowAnalysisAnnotations.NotNull)
Copy link
Contributor

@RikkiGibson RikkiGibson Jan 17, 2020

Choose a reason for hiding this comment

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

Style between this and the above if feels inconsistent #Closed

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, that's on purpose. MaybeNull is two bits, either of which should count (see comment above).
NotNull is also two bits, but we are lenient (we ignore NotNullWhenTrue and NotNullWhenFalse on their own here).


In reply to: 368161275 [](ancestors = 368161275)

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll add a comment


In reply to: 368183067 [](ancestors = 368183067,368161275)

{
// NotNullWhenTrue and NotNullWhenFalse don't count on their own. Only NotNull (ie. both flags) matters.
annotations |= FlowAnalysisAnnotations.DisallowNull;
}
return annotations;
}

private static bool UseLegacyWarnings(BoundExpression expr, TypeWithAnnotations exprType)
{
switch (expr.Kind)
Expand Down Expand Up @@ -6521,6 +6573,7 @@ public override BoundNode VisitIncrementOperator(BoundIncrementOperator node)
public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmentOperator node)
{
var left = node.Left;
var right = node.Right;
Visit(left);
TypeWithAnnotations declaredType = LvalueResultType;
TypeWithAnnotations leftLValueType = declaredType;
Expand Down Expand Up @@ -6553,13 +6606,13 @@ public override BoundNode VisitCompoundAssignmentOperator(BoundCompoundAssignmen
}

TypeWithState resultType;
TypeWithState rightType = VisitRvalueWithState(node.Right);
TypeWithState rightType = VisitRvalueWithState(right);
if ((object)node.Operator.ReturnType != null)
{
if (node.Operator.Kind.IsUserDefined() && (object)node.Operator.Method != null && node.Operator.Method.ParameterCount == 2)
{
MethodSymbol method = node.Operator.Method;
VisitArguments(node, ImmutableArray.Create(node.Left, node.Right), method.ParameterRefKinds, method.Parameters, argsToParamsOpt: default,
VisitArguments(node, ImmutableArray.Create(node.Left, right), method.ParameterRefKinds, method.Parameters, argsToParamsOpt: default,
expanded: true, invokedAsExtensionMethod: false, method);
}

Expand Down Expand Up @@ -6782,11 +6835,6 @@ private Symbol VisitMemberAccess(BoundExpression node, BoundExpression receiverO
}
}

if (_expressionIsRead)
{
ReportMaybeNullFromTypeParameterValueIfNeeded(node, resultType, memberAnnotations);
Copy link
Member Author

@jcouv jcouv Jan 13, 2020

Choose a reason for hiding this comment

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

📝 removed empty method #Resolved

}

SetResult(node, resultType, type);
return member;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1375,12 +1375,12 @@
<note />
</trans-unit>
<trans-unit id="WRN_DisallowNullAttributeForbidsMaybeNullAssignment">
<source>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</source>
<source>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</source>
<target state="needs-review-translation">Do cíle označeného atributem [DisallowNull] se nedá předat možná hodnota null</target>
<note />
</trans-unit>
<trans-unit id="WRN_DisallowNullAttributeForbidsMaybeNullAssignment_Title">
<source>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</source>
<source>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</source>
<target state="needs-review-translation">Do cíle označeného atributem [DisallowNull] se nedá předat možná hodnota null</target>
<note />
</trans-unit>
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1375,12 +1375,12 @@
<note />
</trans-unit>
<trans-unit id="WRN_DisallowNullAttributeForbidsMaybeNullAssignment">
<source>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</source>
<source>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</source>
<target state="needs-review-translation">Ein möglicher NULL-Wert darf nicht an ein Ziel übergeben werden, das mit dem [DisallowNull]-Attribut markiert ist.</target>
<note />
</trans-unit>
<trans-unit id="WRN_DisallowNullAttributeForbidsMaybeNullAssignment_Title">
<source>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</source>
<source>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</source>
<target state="needs-review-translation">Ein möglicher NULL-Wert darf nicht an ein Ziel übergeben werden, das mit dem [DisallowNull]-Attribut markiert ist.</target>
<note />
</trans-unit>
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -1376,12 +1376,12 @@
<note />
</trans-unit>
<trans-unit id="WRN_DisallowNullAttributeForbidsMaybeNullAssignment">
<source>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</source>
<source>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</source>
<target state="needs-review-translation">No se puede pasar un posible valor NULL a un destino marcado con el atributo [DisallowNull]</target>
<note />
</trans-unit>
<trans-unit id="WRN_DisallowNullAttributeForbidsMaybeNullAssignment_Title">
<source>A possible null value may not be assigned to a target marked with the [DisallowNull] attribute</source>
<source>A possible null value may not be used for a type marked with [NotNull] or [DisallowNull]</source>
<target state="needs-review-translation">No se puede pasar un posible valor NULL a un destino marcado con el atributo [DisallowNull]</target>
<note />
</trans-unit>
Expand Down
Loading