Enforce conditional attributes within method bodies#41098
Enforce conditional attributes within method bodies#41098jcouv merged 6 commits intodotnet:masterfrom
Conversation
2d83f8e to
db14972
Compare
db14972 to
7c15d31
Compare
|
Tagging @cston @RikkiGibson since reviewed previous PR. |
| } | ||
|
|
||
| [Fact, WorkItem(39922, "https://github.com/dotnet/roslyn/issues/39922")] | ||
| public void EnforcedInMethodBody_DoesNotReturn() |
There was a problem hiding this comment.
📝 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
There was a problem hiding this comment.
➡️ added test (composes as expected) #Resolved
| #endif | ||
|
|
||
| Environment.FailFast(exception.ToString(), exception); | ||
| throw null!; // to satisfy [DoesNotReturn] |
There was a problem hiding this comment.
| throw null!; // to satisfy [DoesNotReturn] | |
| throw ExceptionUtilities.Unreachable; |
There was a problem hiding this comment.
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?
In reply to: 369127571 [](ancestors = 369127571)
There was a problem hiding this comment.
@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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
➡️ removed comment #Resolved
| value.WasCompilerGenerated || | ||
| !targetType.HasType || | ||
| targetType.Type.IsValueType) | ||
| !ShouldReportNullableAssignment(targetType, valueType.State)) |
There was a problem hiding this comment.
Are there any behavioral changes from this change? #Closed
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
Why do we need to look at the returned expression before conversions are applied? #Closed
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
➡️ fixed comment #Closed
| { | ||
| builder.Append(name); | ||
| builder.Append(state[i] == NullableFlowState.MaybeNull ? "?" : "!"); | ||
| var annotation = (state[i]) switch |
There was a problem hiding this comment.
are the parens necessary? #Closed
| if (!IsConstantFalse(expr)) | ||
| { | ||
| // don't check WhenTrue state on a 'return false;' | ||
| checkConditionalParameterState(node.Syntax, parameters, sense: true); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
Was this incidental to the change, or a result of the NullableWalker change? #Closed
There was a problem hiding this comment.
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'. |
There was a problem hiding this comment.
error [](start = 28, length = 5)
I expected this to be a warning. Did I misinterpret something? #Closed
There was a problem hiding this comment.
Not sure how I messed up the comment. I"ll fix #Closed
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| class C<T, TClass, TNotNull> | ||
| where TClass : class |
There was a problem hiding this comment.
TClass [](start = 10, length = 6)
Consider deleting the TClass from this test. #Closed
There was a problem hiding this comment.
➡️ deleted extraneous type parameter #Closed
| { | ||
| y = x; | ||
| z = x; | ||
| return y != null || z != null; |
There was a problem hiding this comment.
What does the "happy case" for this look like? Perhaps y == null || z == null? Is that test present here? #Closed
| ); | ||
| } | ||
|
|
||
| [Theory] |
There was a problem hiding this comment.
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? s1composes with a[MaybeNullWhen(false)] out string s2#Closed
There was a problem hiding this comment.
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
There was a problem hiding this comment.
➡️ added tests for composition #Resolved
There was a problem hiding this comment.
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)
RikkiGibson
left a comment
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
should throw on all code paths [](start = 48, length = 30)
Is the following a valid use of [DoesNotReturn]?
[DoesNotReturn]
static int F()
{
while (true) { }
}
``` #ClosedThere was a problem hiding this comment.
➡️ added test (no warning on above method) #Closed
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
WRN_MethodShouldThrow [](start = 8, length = 21)
Perhaps WRN_ShouldNotReturn. #Closed
| } | ||
|
|
||
| ImmutableArray<PendingBranch> pendingReturns = base.Scan(ref badRegion); | ||
| EnforceDoesNotReturn(methodMainNode.Syntax.GetLastToken().GetLocation()); |
There was a problem hiding this comment.
methodMainNode.Syntax.GetLastToken().GetLocation() [](start = 33, length = 50)
Consider passing in the method syntax instead rather than calling GetLocation() eagerly. #Closed
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
private [](start = 8, length = 7)
static #Closed
| if (parameters.IsEmpty) | ||
| { | ||
| return; | ||
| } |
There was a problem hiding this comment.
The caller could check this. #Closed
There was a problem hiding this comment.
➡️ makes sense. Done #Resolved
| var annotation = (state[i]) switch | ||
| { | ||
| NullableFlowState.MaybeNull => "?", | ||
| NullableFlowState.MaybeDefault => "??", |
There was a problem hiding this comment.
"??" [](start = 58, length = 4)
Will this be confused for two consecutive slots each MaybeNull? #Closed
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Do we get an analogous warning behavior when a method equivalent to TryGetValue3 calls TryGetValue2? #Closed
| static bool NeverReturns3() | ||
| => throw null!; | ||
|
|
||
| [DoesNotReturn] |
There was a problem hiding this comment.
Is DoesNotReturn supported on property/event accessors? Do we have analogous tests for those scenarios? #Closed
There was a problem hiding this comment.
That is not supported. The attribute has [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
`
In reply to: 369755491 [](ancestors = 369755491)
RikkiGibson
left a comment
There was a problem hiding this comment.
Done with review pass (iteration 2)
|
@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) |
There was a problem hiding this comment.
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);
``` #ClosedThere was a problem hiding this comment.
|
This is a really cool bit of functionality. Thanks for doing this work. #Resolved |
|
@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 |
| bool isUnconvertedBooleanExpression(BoundExpression expr) | ||
| { | ||
| var (expression, _) = RemoveConversion(expr, includeExplicitConversions: true); | ||
| return expression.Type?.SpecialType == SpecialType.System_Boolean; |
There was a problem hiding this comment.
Can this be simplified to expr.Kind != BoundKind.Conversion? At that point, it can be inlined. #Closed
There was a problem hiding this comment.
I worried that we might generate identity conversions.
In reply to: 370273577 [](ancestors = 370273577)
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
continue; [](start = 24, length = 9)
Unnecessary. #Closed
| if (node.RefKind == RefKind.None) | ||
| if (node.RefKind == RefKind.None && | ||
| returnType.Type.SpecialType == SpecialType.System_Boolean && | ||
| isUnconvertedBooleanExpression(expr)) |
There was a problem hiding this comment.
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) | ||
| ); | ||
| } |
There was a problem hiding this comment.
} [](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.
There was a problem hiding this comment.
Added the test
The cast ruins the conditional analysis (ie. no warning on return (bool)true; nor return (bool)false;.
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
DoesNotReturnIfattribute won't have any enforcement. I don't think we can do that type of analysis.