-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Dispose pattern refactor #30437
Dispose pattern refactor #30437
Conversation
- Use BindMethodGroupInvocation to perform lookup, rather than manually iterating with LookupMembersInType - Share the pattern logic with GetPinnableReference lookup - Update tests to match new logic
- Fail refextensionmethod tests + add prototype comment noting it doesn't work.
@dotnet-bot retest ubuntu_16_mono_debug_prtest please |
@dotnet-bot retest windows_release_unit64_prtest please |
@@ -500,7 +497,14 @@ static void Main() | |||
} | |||
} | |||
}"; | |||
CreateCompilation(source).VerifyDiagnostics(); | |||
CreateCompilation(source).VerifyDiagnostics( |
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.
VerifyDiagnostics [](start = 38, length = 17)
📝 this scared me (looked like a breaking change), but this is actually restoring the existing behavior (master
produces error CS1674: 'C2': type used in a using statement must be implicitly convertible to 'System.IDisposable'
) #Resolved
It would be be great to capture the new standard rules of binding to a pattern into a speclet (under Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/UsingStatementTests.cs:1108 in 8755cf0. [](commit_id = 8755cf0, deletion_comment = False) |
Success, | ||
|
||
/// <summary> | ||
/// A member was looked up, but it was not a method |
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.
looked up [](start = 25, length = 9)
nit: "found" instead of "looked up" (also below) #Resolved
diagnostics.Add(declarationSyntax, useSiteDiagnostics); | ||
|
||
if (!iDisposableConversion.IsImplicit) | ||
{ | ||
disposeMethod = TryFindDisposePatternMethod(declTypeExpr, declarationSyntax, diagnostics); | ||
disposeMethod = TryFindDisposePatternMethod(declaration.InitializerOpt, declarationSyntax, diagnostics); |
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.
declaration.InitializerOpt [](start = 63, length = 26)
I'm surprised that we look at the initializer expression, rather than the declared type.
using (TypeWithDispose x = new TypeWithoutDispose()) ...
(with a user-defined conversion from one type to another) would be a relevant scenario.
You may need to introduce a placeholder bound node (a node with just a type) to adapt to the method API.
Update: you may not need a placeholder. Maybe declarations[0].DeclaredType
would work?
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.
Yes, it feels like we should use the declared type Today, the second using
is an error:
using (FileStream x = new FileStream("", FileMode.Open)) { }
using (object y = new FileStream("", FileMode.Open)) { } // error
In reply to: 224899109 [](ancestors = 224899109)
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.
Fixed + added tests to cover.
{ | ||
diagnostics.Add(ErrorCode.WRN_PatternIsAmbiguous, syntaxExpr.Location, typeExpr.Type, messageID.Localize(), | ||
resolution.OverloadResolutionResult.Results[0].Member, resolution.OverloadResolutionResult.Results[1].Member); | ||
// bound to something uncallable |
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.
uncallable [](start = 42, length = 10)
The comment seems out of date, as it seems to better describe return PatternLookupResult.NotCallable;
#Closed
out var disposeMethod); | ||
|
||
|
||
if (disposeMethod?.ReturnsVoid == false || result == PatternLookupResult.NotAMethod) |
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.
result == PatternLookupResult.NotAMethod [](start = 55, length = 40)
What about other possible result values? (such as PatternLookupResult.NotCallable
)
Is it better to report such cases as pattern mismatch or no conversion to IDisposable?
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.
In all cases other than NotAMethod, the bindmethodgroup will add applicable errors. This case handles when Dispose wasn't a method (so we didn't try and bind), or when x.Dispose() was a valid call, but didn't fit the pattern: I'm clarifying the logic in the speclet.
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.
For such situations in other contexts, I used enums with 3 values: Success, Failed, FailedNotReported. Consider whether that is applicable here, or if we care about the more detailed results with specific semantics.
In reply to: 225302376 [](ancestors = 225302376)
finally | ||
|
||
if (HasOptionalOrVariableParameters(patterMethodSymbol) || | ||
patterMethodSymbol.ReturnsVoid || |
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.
patterMethodSymbol [](start = 16, length = 18)
typo: "pattern" #Closed
@@ -3301,110 +3219,89 @@ internal virtual ImmutableArray<LabelSymbol> Labels | |||
/// Perform a lookup for the specified method on the specified type, searching further if the first resolved symbol doesn't match |
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.
specified type [](start = 61, length = 14)
comment may be out-of-date
out var disposeMethod); | ||
|
||
|
||
if (disposeMethod?.ReturnsVoid == false || result == PatternLookupResult.NotAMethod) |
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.
if [](start = 12, length = 2)
nit: double empty line above #Closed
} | ||
|
||
return patterMethodSymbol; | ||
if (result != PatternLookupResult.Success) |
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.
Consider replacing this with (object)patternMethodSymbol != null
in the here. That way it's clear that if
belowpatternMethodSymbol
is not null when it is dereferenced in if
below. #Closed
{ | ||
Debug.Assert(lookupResult.IsClear); | ||
// PROTOTYPE: try and resolve the method using binding |
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.
try and resolve the method using binding [](start = 26, length = 41)
📝 I didn't understand this PROTOTYPE comment. Could you clarify?
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.
We were originally doing lookup by manually iterating the members. This was a prototype to try and find the pattern by instead assuming it exists and attempt to bind the method group invocation. In implementing it, I've become fairly convinced this is the right direction, so i'll remove the prototype comment.
indexed: false, | ||
bindingDiagnostics); | ||
|
||
BoundMethodGroup bmg = new BoundMethodGroup(syntaxNode, default, receiver, "Dispose", ImmutableArray<MethodSymbol>.Empty, LookupResult.GetInstance(), BoundMethodGroupFlags.SearchExtensionMethods, false); |
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.
default [](start = 72, length = 7)
Consider naming default
and false
arguments. #Closed
indexed: false, | ||
bindingDiagnostics); | ||
|
||
BoundMethodGroup bmg = new BoundMethodGroup(syntaxNode, default, receiver, "Dispose", ImmutableArray<MethodSymbol>.Empty, LookupResult.GetInstance(), BoundMethodGroupFlags.SearchExtensionMethods, false); |
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.
"Dispose" [](start = 91, length = 9)
Should be methodName
. Please add test to demonstrate this issue. #Closed
indexed: false, | ||
bindingDiagnostics); | ||
|
||
BoundMethodGroup bmg = new BoundMethodGroup(syntaxNode, default, receiver, "Dispose", ImmutableArray<MethodSymbol>.Empty, LookupResult.GetInstance(), BoundMethodGroupFlags.SearchExtensionMethods, false); |
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.
nit: acronym variables are not common in compiler code. Let's give a proper name.
nit: var
could help avoid redundant type name. #Closed
indexed: false, | ||
bindingDiagnostics); | ||
|
||
BoundMethodGroup bmg = new BoundMethodGroup(syntaxNode, default, receiver, "Dispose", ImmutableArray<MethodSymbol>.Empty, LookupResult.GetInstance(), BoundMethodGroupFlags.SearchExtensionMethods, false); |
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.
bmg [](start = 33, length = 3)
Never mind, it looks like bmg
is not referenced. Let's remove it. #Closed
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.
Oops. left that in by mistake. Good catch, thanks #Closed
if (disposeMethod?.ReturnsVoid == false || result == PatternLookupResult.NotAMethod) | ||
{ | ||
ReportPatternWarning(diagnostics, expr.Type, disposeMethod, syntaxNode, MessageID.IDS_Disposable); | ||
disposeMethod = null; | ||
} |
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.
} [](start = 12, length = 1)
For binding fixed
pattern, we do some extra checks (copied below). Just want to confirm that we're intentionally not doing those checks for using
pattern.
It would be good to have a test for static
Dispose
method (that's not an extension).
if (HasOptionalOrVariableParameters(patterMethodSymbol) ||
patterMethodSymbol.ReturnsVoid ||
!patterMethodSymbol.RefKind.IsManagedReference() ||
!(patterMethodSymbol.ParameterCount == 0 || patterMethodSymbol.IsStatic && patterMethodSymbol.ParameterCount == 1))
{
// the method does not fit the pattern
additionalDiagnostics.Add(ErrorCode.WRN_PatternBadSignature, initializer.Syntax.Location, initializer.Type, "fixed", patterMethodSymbol);
return null;
}
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.
Generalized pattern lookup has two phases: lookup and verification. We're sharing the lookup phases, but each pattern has its own unique verification. In this case dispose is a bit more lenient than fixed.
Added a test for the static method case.
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.
Done with review pass (iteration 3).
} | ||
else | ||
|
||
// we have succeeded or almost succeded to bind the method |
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.
succeded [](start = 47, length = 8)
Typo
// using (C1 c = new C1()) | ||
Diagnostic(ErrorCode.WRN_PatternIsAmbiguous, "C1 c = new C1()").WithArguments("C1", "disposable", "C1.Dispose()", "C1.Dispose()").WithLocation(13, 16), | ||
// (13,16): error CS1674: 'C1': type used in a using statement must have a public void-returning Dispose() instance method. | ||
Diagnostic(ErrorCode.ERR_AmbigCall, "C1 c = new C1()").WithArguments("C1.Dispose()", "C1.Dispose()").WithLocation(13, 16), |
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.
ERR_AmbigCall [](start = 37, length = 13)
We're now reporting that "the call is ambiguous" rather than the pattern method is ambiguous. Are all of uses of FindPatternMethodRelaxed
for calls to the pattern method?
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.
Yes, this is consistent with how GetPinnableReference works today, and that's what we're basing the generalized pattern lookup off at the moment (it may change after review/LDM).
Removed the "personal review" label since it looks like the PR is ready for review. #Resolved |
- Make a bound local to be the receiver - Add tests for ref/in extension methods - Add tests for implict conversions
- Add a static pattern dispose test - Fix a typo
@dotnet-bot retest ubuntu_16_mono_debug_prtest please |
1 similar comment
@dotnet-bot retest ubuntu_16_mono_debug_prtest please |
@dotnet/roslyn-compiler for another round of reviews please |
{ | ||
AnalyzedArguments arguments = AnalyzedArguments.GetInstance(); | ||
MethodSymbol patternMethod = null; | ||
if (patternMethodCall.Kind != BoundKind.Call) |
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.
Just for my edification--what other BoundKinds could be found here that would result in returning NotCallable?
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.
It could also be a DynamicInvocation if any of the arguments are dynamic. In the dispose case it can't be (we never pass params) but in the general case its something we have to consider.
I should add a test for an optional dynamic arg though, as that should work ok.
BoundTypeExpression declTypeExpr = declarations[0].DeclaredType; | ||
BoundLocalDeclaration declaration = declarations[0]; | ||
TypeSymbol declType = declaration.DeclaredType.Type; | ||
BoundLocal declarationLocal = new BoundLocal(declaration.Syntax, declaration.LocalSymbol, null, declType); |
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.
BoundLocal declarationLocal [](start = 12, length = 27)
Can be moved inside if (!iDisposableConversion.IsImplicit) { ... }
.
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.
Done
/// <returns>The <see cref="MethodSymbol"/> of the Dispose method if one is found, otherwise null.</returns> | ||
internal MethodSymbol TryFindDisposePatternMethod(BoundExpression expr, SyntaxNode syntaxNode, DiagnosticBag diagnostics) | ||
{ | ||
if(expr?.Type is null) |
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.
expr [](start = 15, length = 4)
Is expr
ever null
?
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.
It shouldn't be. I've added an assert to declare the precondition.
- Reduce scope of local - Remove null check ( + add debug assert)
@@ -281,6 +281,64 @@ static void Main() | |||
CreateCompilation(source).VerifyDiagnostics(); | |||
} | |||
|
|||
[Fact] | |||
public void UsingPatternLessDerivedAssignement() |
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.
Assignement [](start = 43, length = 11)
Typo
@dotnet-bot retest ubuntu_16_mono_debug_prtest please |
@dotnet-bot retest windows_debug_vs-integration_prtest please |
* Refactor pattern lookup logic: - Use BindMethodGroupInvocation to perform lookup, rather than manually iterating with LookupMembersInType - Share the pattern logic with GetPinnableReference lookup - Update existing tests to match new logic - Add new tests
Refactor pattern lookup logic:
This change aims to reduce the duplication we have for looking up pattern based methods, and attempt to set a path for future lookups. It makes the dispose lookup work in the same way we currently lookup GetPinnableReference, meaning we support any candidate that would be equivalent to writing
c.Dispose()
in code: essentially we attempt to perform binding on what we'll eventually lower to and report the results.Previously we gave warnings about e.g. ambiguous candidates, or inaccessible methods; because we're actually doing an invocation binding these are now reported as errors as if the user had actually invoked it. This is actually consistent with the behavior we see when looking up Deconstruct and GetPinnableReference, so while different (and possibly not as nice?) it's at least consistent with what we do in other pattern lookups.
We'll still report a warning (and an overall error) when binding succeeded but the thing that was bound doesn't fit the pattern.