Skip to content

Enforce conditional attributes within method bodies#41098

Merged
jcouv merged 6 commits intodotnet:masterfrom
jcouv:cond-attributes
Jan 24, 2020
Merged

Enforce conditional attributes within method bodies#41098
jcouv merged 6 commits intodotnet:masterfrom
jcouv:cond-attributes

Conversation

@jcouv
Copy link
Copy Markdown
Member

@jcouv jcouv commented Jan 20, 2020

Fixes the second part of #36039 (enforcing nullability attributes in method bodies) by handling conditional attributes.

My previous PR (#40789) handled unconditional attributes and implemented a simple behavior for conditional attributes (ie. treat them unconditionally and leniently).

Note: the DoesNotReturnIf attribute won't have any enforcement. I don't think we can do that type of analysis.

@jcouv jcouv added this to the 16.5.P3 milestone Jan 20, 2020
@jcouv jcouv self-assigned this Jan 20, 2020
@jcouv jcouv force-pushed the cond-attributes branch 2 times, most recently from 2d83f8e to db14972 Compare January 21, 2020 05:25
@jcouv jcouv marked this pull request as ready for review January 21, 2020 06:21
@jcouv jcouv requested review from a team as code owners January 21, 2020 06:21
@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Jan 21, 2020

Tagging @cston @RikkiGibson since reviewed previous PR.
FYI @sharwell #Resolved

}

[Fact, WorkItem(39922, "https://github.com/dotnet/roslyn/issues/39922")]
public void EnforcedInMethodBody_DoesNotReturn()
Copy link
Copy Markdown
Contributor

@sharwell sharwell Jan 21, 2020

Choose a reason for hiding this comment

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

📝 Need tests for DoesNotReturn calling another DoesNotReturn, e.g.:

[DoesNotReturn]
static void Fail() => Fail("message"); // should not warn

[DoesNotReturn]
static void Fail(string message) => throw null!;
``` #Resolved

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ added test (composes as expected) #Resolved

#endif

Environment.FailFast(exception.ToString(), exception);
throw null!; // to satisfy [DoesNotReturn]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw null!; // to satisfy [DoesNotReturn]
throw ExceptionUtilities.Unreachable;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This might be a good place for #pragma warning disable. In the future when we adopt the newer TFM we should get the [DoesNotReturn] attribute on Environment.FailFast and we can delete the pragma. @JoeRobich don't we have a mechanism to periodically search our sources for disabled warnings and turn them back on?

See https://github.com/dotnet/runtime/blob/4f9ae42d861fcb4be2fcd5d3d55d5f227d30e723/src/coreclr/src/System.Private.CoreLib/src/System/Environment.CoreCLR.cs#L52


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

Copy link
Copy Markdown
Member

@JoeRobich JoeRobich Jan 22, 2020

Choose a reason for hiding this comment

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

@JoeRobich don't we have a mechanism to periodically search our sources for disabled warnings and turn them back on?

@RikkiGibson If we did, I would like to know about it. Find in Files maybe? #Resolved

}
}
";
// Note: this is a situation where it is annoying that we're learning from pure null tests
Copy link
Copy Markdown
Contributor

@sharwell sharwell Jan 21, 2020

Choose a reason for hiding this comment

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

I'm not sure I agree with this note. Each of the warnings is code that is at best misleading, and a likely source of annotation errors that could cascade once the attribute checks are fully implemented. #Closed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Chatted offline with @jcouv and it's feeling like these warnings are good.


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

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ removed comment #Resolved

@RikkiGibson RikkiGibson self-requested a review January 21, 2020 18:40
value.WasCompilerGenerated ||
!targetType.HasType ||
targetType.Type.IsValueType)
!ShouldReportNullableAssignment(targetType, valueType.State))
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

Are there any behavioral changes from this change? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

No, this is just extracting the logic so that it can be called from the new checks. #Closed

if (node.RefKind == RefKind.None)
if (node.RefKind == RefKind.None &&
returnType.Type.SpecialType == SpecialType.System_Boolean &&
isUnconvertedBooleanExpression(expr))
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

Why do we need to look at the returned expression before conversions are applied? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

If there is a conversion from some custom type to bool, then we can't do the new analysis. We don't know what logic that conversion might contain. We lose track of whenTrue/False branches in conversions, since it's custom logic.

Also imagine if that conversion produced nullability warnings, we'd want to run it through VisitImplicitConversion. #Closed

}

/// <summary>
/// Can we assign this state into this type without a warning?
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

It feels like this doc comment is asking the opposite question of what the method is asking (should I assign without a warning, versus should I report) #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ fixed comment #Closed

{
builder.Append(name);
builder.Append(state[i] == NullableFlowState.MaybeNull ? "?" : "!");
var annotation = (state[i]) switch
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

are the parens necessary? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ removed #Closed

if (!IsConstantFalse(expr))
{
// don't check WhenTrue state on a 'return false;'
checkConditionalParameterState(node.Syntax, parameters, sense: true);
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

sense [](start = 84, length = 5)

It would be good to have a more descriptive name for this parameter. Maybe possibleReturnValue? (not in love with that name either, though..) #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

I don't love it either :-$
The reason I used "sense" is because I've seen it used in other places for such situations (applying some logic either to the true or the false branch). #Closed

&& attr.CommonConstructorArguments.Length == 1
&& attr.CommonConstructorArguments[0].Kind == TypedConstantKind.Type)
{
builderArgument = attr.CommonConstructorArguments[0].ValueInternal;
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

Was this incidental to the change, or a result of the NullableWalker change? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

Without this, we can't bootstrap. The compiler would complain that buildArgument is null upon exit.
But we know it's not null in this case, because we checked the .Kind above. #Closed

var comp = CreateNullableCompilation(new[] { source, MaybeNullWhenAttributeDefinition });
comp.VerifyDiagnostics();
comp.VerifyDiagnostics(
// (11,13): error CS8762: Parameter 's' may not have a null value when exiting with 'false'.
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

error [](start = 28, length = 5)

I expected this to be a warning. Did I misinterpret something? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

Not sure how I messed up the comment. I"ll fix #Closed

using System.Diagnostics.CodeAnalysis;

class C<T, TClass, TNotNull>
where TClass : class
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

TClass [](start = 10, length = 6)

Consider deleting the TClass from this test. #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ deleted extraneous type parameter #Closed

{
y = x;
z = x;
return y != null || z != null;
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

What does the "happy case" for this look like? Perhaps y == null || z == null? Is that test present here? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ added test #Closed

);
}

[Theory]
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

Some more cases I'd like to see coverage for:

  • Suppression (am I expected to put ! on the value being assigned? or on the expression being returned?)
  • Composition
    • Should be able to use a decorator pattern on methods with equivalent sets of MaybeNullWhen/NotNullWhen attributes
    • Should be able to compose methods with "equivalent" combinations of attributes e.g. a [NotNullWhen(true)] out string? s1 composes with a [MaybeNullWhen(false)] out string s2 #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

In terms of suppression, you can't suppress this new warning directly (except maybe with #nullable disable warnings or explicit diagnostic suppression). You can also put non-null values into the parameters in question, with ! suppressions if needed.

I'll add tests for composition. Could you clarify the decorator scenario you had in mind? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ added tests for composition #Resolved

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was thinking of:

using System.Collections.Generic;

class DictionaryDecorator
{
    readonly Dictionary<string, string> _dict;
    DictionaryDecorator(Dictionary<string, string> dict)
    {
        _dict = dict;
    }

    public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value)
    {
        return _dict.TryGetValue(key, out value);
    }
}

which is not all that different from simple method composition, I'll grant :)


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

Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Done with review pass (iteration 1)

<value>Parameter may not have a null value when exiting in some condition.</value>
</data>
<data name="WRN_MethodShouldThrow" xml:space="preserve">
<value>A method marked with [DoesNotReturn] should throw on all code paths.</value>
Copy link
Copy Markdown
Contributor

@cston cston Jan 22, 2020

Choose a reason for hiding this comment

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

should throw on all code paths [](start = 48, length = 30)

Is the following a valid use of [DoesNotReturn]?

[DoesNotReturn]
static int F()
{
    while (true) { }
}
``` #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ added test (no warning on above method) #Closed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry, I wasn't clear. I meant a method marked [DoesNotReturn] does not need to throw on all code paths. Perhaps the message should be "A method marked [DoesNotReturn] should not return."


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

ERR_ExternEventInitializer = 8760,
ERR_AmbigBinaryOpsOnUnconstrainedDefault = 8761,
WRN_ParameterConditionallyDisallowsNull = 8762,
WRN_MethodShouldThrow = 8763,
Copy link
Copy Markdown
Contributor

@cston cston Jan 22, 2020

Choose a reason for hiding this comment

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

WRN_MethodShouldThrow [](start = 8, length = 21)

Perhaps WRN_ShouldNotReturn. #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ done #Resolved

}

ImmutableArray<PendingBranch> pendingReturns = base.Scan(ref badRegion);
EnforceDoesNotReturn(methodMainNode.Syntax.GetLastToken().GetLocation());
Copy link
Copy Markdown
Contributor

@cston cston Jan 22, 2020

Choose a reason for hiding this comment

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

methodMainNode.Syntax.GetLastToken().GetLocation() [](start = 33, length = 50)

Consider passing in the method syntax instead rather than calling GetLocation() eagerly. #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

I don't think I could do that, because the check on end of method uses the last token of the syntax, instead of a SyntaxNode #Resolved

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't follow. Couldn't the method be defined as:

void EnforceDoesNotReturn(SyntaxNode syntax)
{
    ...
    ReportDiagnostic(ErrorCode.WRN_ShouldNotReturn,
        syntax.GetLastToken().GetLocation());
}

---
In reply to: [369381283](https://github.com/dotnet/roslyn/pull/41098#discussion_r369381283) [](ancestors = 369381283)

/// <summary>
/// Can we assign this state into this type without a warning?
/// </summary>
private bool ShouldReportNullableAssignment(TypeWithAnnotations type, NullableFlowState state)
Copy link
Copy Markdown
Contributor

@cston cston Jan 22, 2020

Choose a reason for hiding this comment

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

private [](start = 8, length = 7)

static #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ done #Resolved

if (parameters.IsEmpty)
{
return;
}
Copy link
Copy Markdown
Contributor

@cston cston Jan 22, 2020

Choose a reason for hiding this comment

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

The caller could check this. #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

➡️ makes sense. Done #Resolved

var annotation = (state[i]) switch
{
NullableFlowState.MaybeNull => "?",
NullableFlowState.MaybeDefault => "??",
Copy link
Copy Markdown
Contributor

@cston cston Jan 22, 2020

Choose a reason for hiding this comment

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

"??" [](start = 58, length = 4)

Will this be confused for two consecutive slots each MaybeNull? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Jan 22, 2020

Choose a reason for hiding this comment

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

It wasn't confusing because we print variable names and annotations. So it shows x?y??z! for example #Resolved

{
y = x;
z = x;
return y != null && z != null;
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

It would be handy to have // 1, // 2 comments in this test #Closed


static bool TryGetValue2([NotNullWhen(true)] out TYPE y)
{
return TryGetValue3(out y); // 1
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

Do we get an analogous warning behavior when a method equivalent to TryGetValue3 calls TryGetValue2? #Closed

static bool NeverReturns3()
=> throw null!;

[DoesNotReturn]
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

Is DoesNotReturn supported on property/event accessors? Do we have analogous tests for those scenarios? #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That is not supported. The attribute has [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
`


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

Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

Done with review pass (iteration 2)

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Jan 22, 2020

@RikkiGibson @cston Please take another look when you get the chance. Thanks #Resolved

[InlineData("TStruct?")]
[InlineData("TClass?")]
[WorkItem(39922, "https://github.com/dotnet/roslyn/issues/39922")]
public void EnforcedInMethodBody_NotNullWhen_Composition(string type)
Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson Jan 22, 2020

Choose a reason for hiding this comment

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

I can't find tests which demonstrate the expected behavior around composition like the following:

bool TryGetValue1(string key, [MaybeNullWhen(false)] out string value) => TryGetValue2(key, out value);
bool TryGetValue2(string key, [NotNullWhen(true)] out string? value) => TryGetValue1(key, out value);
``` #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added. Thanks


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

Copy link
Copy Markdown
Member

@RikkiGibson RikkiGibson left a comment

Choose a reason for hiding this comment

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

:shipit:

@RikkiGibson
Copy link
Copy Markdown
Member

RikkiGibson commented Jan 23, 2020

This is a really cool bit of functionality. Thanks for doing this work. #Resolved

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Jan 23, 2020

@RikkiGibson Haha, thanks. This PR reminds me of an earlier one that made ‘Debug.Assert(!string.IsNullOrEmpty(x))’ work. It’s nice when things fall nicely together ;) #Resolved

@jcouv jcouv requested a review from cston January 23, 2020 16:54
bool isUnconvertedBooleanExpression(BoundExpression expr)
{
var (expression, _) = RemoveConversion(expr, includeExplicitConversions: true);
return expression.Type?.SpecialType == SpecialType.System_Boolean;
Copy link
Copy Markdown
Contributor

@cston cston Jan 23, 2020

Choose a reason for hiding this comment

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

Can this be simplified to expr.Kind != BoundKind.Conversion? At that point, it can be inlined. #Closed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I worried that we might generate identity conversions.


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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. This check was unnecessary after all. Removed.


In reply to: 370294344 [](ancestors = 370294344,370273577)

{
// Parameter '{name}' may not have a null value when exiting with '{sense}'.
ReportDiagnostic(ErrorCode.WRN_ParameterConditionallyDisallowsNull, syntax.Location, parameter.Name, sense ? "true" : "false");
continue;
Copy link
Copy Markdown
Contributor

@cston cston Jan 23, 2020

Choose a reason for hiding this comment

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

continue; [](start = 24, length = 9)

Unnecessary. #Closed

@cston
Copy link
Copy Markdown
Contributor

cston commented Jan 23, 2020

        return true;

Are we testing s = null; return true;? #Closed


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs:20739 in 59b3c0e. [](commit_id = 59b3c0e, deletion_comment = False)

if (node.RefKind == RefKind.None)
if (node.RefKind == RefKind.None &&
returnType.Type.SpecialType == SpecialType.System_Boolean &&
isUnconvertedBooleanExpression(expr))
Copy link
Copy Markdown
Contributor

@cston cston Jan 23, 2020

Choose a reason for hiding this comment

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

isUnconvertedBooleanExpression(expr) [](start = 20, length = 36)

Are we testing this case? (If this check is removed, would tests fail?) #Pending

// return false; // 1
Diagnostic(ErrorCode.WRN_ParameterConditionallyDisallowsNull, "return false;").WithArguments("s", "false").WithLocation(11, 13)
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

} [](start = 8, length = 1)

Please test:

static bool TryGetValue([MaybeNullWhen(false)]out string s)
{
    s = null;
    return (bool)true;
}

It doesn't necessarily need to warn but we should test the behavior.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the test
The cast ruins the conditional analysis (ie. no warning on return (bool)true; nor return (bool)false;.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants