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

Dispose pattern refactor #30437

Merged

Conversation

chsienki
Copy link
Contributor

Refactor pattern lookup logic:

  • Use BindMethodGroupInvocation to perform lookup, rather than manually iterating with LookupMembersInType
  • Share the pattern logic with GetPinnableReference lookup
  • Update tests to match new 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.

- Use BindMethodGroupInvocation to perform lookup, rather than manually iterating with LookupMembersInType
- Share the pattern logic with GetPinnableReference lookup
- Update tests to match new logic
@chsienki chsienki requested a review from a team as a code owner October 10, 2018 22:21
@chsienki chsienki added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Oct 10, 2018
- Fail refextensionmethod tests + add prototype comment noting it doesn't work.
@chsienki
Copy link
Contributor Author

@dotnet-bot retest ubuntu_16_mono_debug_prtest please

@chsienki
Copy link
Contributor Author

@dotnet-bot retest windows_release_unit64_prtest please

@@ -500,7 +497,14 @@ static void Main()
}
}
}";
CreateCompilation(source).VerifyDiagnostics();
CreateCompilation(source).VerifyDiagnostics(
Copy link
Member

@jcouv jcouv Oct 12, 2018

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

@jcouv
Copy link
Member

jcouv commented Oct 12, 2018

    public void UsingPatternWithParamsTest()

It would be be great to capture the new standard rules of binding to a pattern into a speclet (under docs/features), if you don't have one already.


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
Copy link
Member

@jcouv jcouv Oct 12, 2018

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);
Copy link
Member

@jcouv jcouv Oct 12, 2018

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?

Copy link
Member

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)

Copy link
Contributor Author

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
Copy link
Member

@jcouv jcouv Oct 12, 2018

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)
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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 ||
Copy link
Member

@jcouv jcouv Oct 12, 2018

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
Copy link
Member

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)
Copy link
Member

@jcouv jcouv Oct 12, 2018

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)
Copy link
Member

@jcouv jcouv Oct 12, 2018

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 if below here. That way it's clear that patternMethodSymbol is not null when it is dereferenced in if below. #Closed

{
Debug.Assert(lookupResult.IsClear);
// PROTOTYPE: try and resolve the method using binding
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

@jcouv jcouv Oct 12, 2018

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);
Copy link
Member

@jcouv jcouv Oct 12, 2018

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);
Copy link
Member

@jcouv jcouv Oct 12, 2018

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);
Copy link
Member

@jcouv jcouv Oct 12, 2018

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

Copy link
Contributor Author

@chsienki chsienki Oct 15, 2018

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;
}
Copy link
Member

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;
                }

Copy link
Contributor Author

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.

@jcouv jcouv self-assigned this Oct 12, 2018
Copy link
Member

@jcouv jcouv 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 3).

}
else

// we have succeeded or almost succeded to bind the method
Copy link
Member

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),
Copy link
Member

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?

Copy link
Contributor Author

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).

@jcouv jcouv removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Oct 16, 2018
@jcouv
Copy link
Member

jcouv commented Oct 16, 2018

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
@chsienki
Copy link
Contributor Author

chsienki commented Nov 1, 2018

@dotnet-bot retest ubuntu_16_mono_debug_prtest please

1 similar comment
@chsienki
Copy link
Contributor Author

chsienki commented Nov 2, 2018

@dotnet-bot retest ubuntu_16_mono_debug_prtest please

@chsienki
Copy link
Contributor Author

chsienki commented Nov 5, 2018

@dotnet/roslyn-compiler for another round of reviews please

{
AnalyzedArguments arguments = AnalyzedArguments.GetInstance();
MethodSymbol patternMethod = null;
if (patternMethodCall.Kind != BoundKind.Call)
Copy link
Contributor

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?

Copy link
Contributor Author

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);
Copy link
Member

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) { ... }.

Copy link
Contributor Author

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)
Copy link
Member

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?

Copy link
Contributor Author

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()
Copy link
Member

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

@cston
Copy link
Member

cston commented Nov 7, 2018

public void Dispose() { }

Consider testing Dispose<T>().


Refers to: src/Compilers/CSharp/Test/Semantic/Semantics/UsingStatementTests.cs:99 in dac5bb9. [](commit_id = dac5bb9, deletion_comment = False)

@chsienki
Copy link
Contributor Author

chsienki commented Nov 8, 2018

@dotnet-bot retest ubuntu_16_mono_debug_prtest please

@chsienki
Copy link
Contributor Author

chsienki commented Nov 8, 2018

@dotnet-bot retest windows_debug_vs-integration_prtest please

@chsienki chsienki merged this pull request into dotnet:features/enhanced-using Nov 8, 2018
chsienki added a commit to chsienki/roslyn that referenced this pull request Nov 8, 2018
* 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
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.

None yet

4 participants