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

Allow dispose pattern to be implemented via extension method #29307

Conversation

chsienki
Copy link
Contributor

  • Add option to FindPatternMethod to allow searching for extension methods
  • Generate correct call to dispose when it is an extension method
  • Add various tests

@chsienki chsienki requested review from a team as code owners August 14, 2018 23:28
@@ -148,6 +148,7 @@ function Ensure-DotnetSdk() {
Create-Directory $cliDir
Create-Directory $toolsDir
$destFile = Join-Path $toolsDir "dotnet-install.ps1"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Copy link
Member

@jcouv jcouv Aug 14, 2018

Choose a reason for hiding this comment

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

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 [](start = 8, length = 79)

Not sure what happened in this file. Should be reverted.

Update: it looks like there is a TLS 1.2 commit that doesn't belong in your PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I pulled this in from master, as the build doesn't work without it

Copy link
Member

Choose a reason for hiding this comment

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

This is a necessary change. The servers that we get a lot of our bootstrapping tools from recently changed to require Tls12 which is not the default. Every branch has needed to cherry-pick this fix in order to pass CI.

Copy link
Member

Choose a reason for hiding this comment

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

I think we should just perform a merge from master into this branch. I'll queue up a PR to add it to the automerge list as well.

@jcouv jcouv added this to the 16.0 milestone Aug 15, 2018
SyntaxTree syntaxTree, MessageID messageID)
{
Debug.Assert(lookupResult.IsClear);


//prototype:
Copy link
Member

Choose a reason for hiding this comment

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

Nit: use PROTOTYPE. Stands out more.

}
}

MethodSymbol patternMethod = PerformPatternOverloadResolution(patternType, candidateMethods, syntaxExpr, warningsOnly, diagnostics, syntaxTree, messageID);

patternMethod = PerformPatternOverloadResolution(patternType, candidateMethods, syntaxExpr, warningsOnly, diagnostics, syntaxTree, messageID);
candidateMethods.Free();
Copy link
Member

Choose a reason for hiding this comment

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

Double blank line below.



//prototype:
LookupOptions lookupOptions = (searchExtensionMethods) ? LookupOptions.MustBeInvocableIfMember : LookupOptions.Default;
Copy link
Member

Choose a reason for hiding this comment

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

MustBeInvocableIfMember allows for items like fields / properties where the type is dynamic or it's a delegate instance. Please add tests to ensure this doesn't work with the pattern based disposed feature.

originalBinder: this,
diagnose: false,
useSiteDiagnostics: ref useSiteDiagnostics);

diagnostics.Add(syntaxExpr, useSiteDiagnostics);

if (!lookupResult.IsMultiViable)
MethodSymbol patternMethod = null;
Copy link
Member

Choose a reason for hiding this comment

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

r [](start = 30, length = 1)

This declaration can be moved to the initialization on line 3252. It has no usage before then.

candidateMethods.Free();


// if we failed to find a method via normal lookup, see if there is an extension method that satisifies it
if (patternMethod == null && searchExtensionMethods)
Copy link
Member

Choose a reason for hiding this comment

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

Use pattern is null instead of == null if you want to determine if the reference has the value null.

@jaredpar
Copy link
Member

                    ReportPatternWarning(diagnostics, patternType, member, syntaxExpr, messageID);

Won't this warn when there is both a field (via dynamic) and method candidate? I would assume in that case we would prefer the method candidate in the case it was more applicable.


Refers to: src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs:3234 in 2abef17. [](commit_id = 2abef17, deletion_comment = False)

if (patternMethod == null && searchExtensionMethods)
{
var extLookupResult = LookupResult.GetInstance();
this.LookupExtensionMethods(extLookupResult, methodName, 0, lookupOptions, ref useSiteDiagnostics);
Copy link
Member

Choose a reason for hiding this comment

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

Please used a named argument for 0 here to give it meaning at the call site.


extLookupResult.Free();

if (patternMethod == null)
Copy link
Member

Choose a reason for hiding this comment

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

Use is null here as well.

@@ -702,7 +702,7 @@ internal MethodSymbol TryFindDisposePatternMethod(TypeSymbol exprType, SyntaxNod
{
LookupResult lookupResult = LookupResult.GetInstance();

MethodSymbol disposeMethod = FindPatternMethod(exprType, WellKnownMemberNames.DisposeMethodName, lookupResult, syntaxNode, warningsOnly: true, diagnostics, syntaxNode.SyntaxTree, MessageID.IDS_Disposable);
MethodSymbol disposeMethod = FindPatternMethod(exprType, WellKnownMemberNames.DisposeMethodName, lookupResult, syntaxNode, searchExtensionMethods: true, warningsOnly: true, diagnostics, syntaxNode.SyntaxTree, MessageID.IDS_Disposable);
Copy link
Member

@agocke agocke Aug 15, 2018

Choose a reason for hiding this comment

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

Could you reformat this like:

    exprType, 
    WellKnownMemberNames.DisposeMethodName, 
    lookupResult, 
    syntaxNode, 
    searchExtensionMethods: true,
    warningsOnly: true,
    diagnostics,
    syntaxNode.SyntaxTree,
    MessageID.IDS_Disposable)

The line is so long right now that it's pretty unreadable.

@@ -3184,15 +3184,19 @@ internal virtual ImmutableArray<LabelSymbol> Labels
/// <param name="patternType">Type to search.</param>
/// <param name="methodName">Method to search for.</param>
/// <param name="lookupResult">Passed in for reusability.</param>
/// <param name="searchExtensionMethods">True if the lookup should consider extension methods as viable candidates</param>
/// <param name="warningsOnly">True if failures should result in warnings; false if they should result in errors.</param>
/// <param name="diagnostics">Populated with binding diagnostics.</param>
/// <returns>The desired method or null.</returns>
internal MethodSymbol FindPatternMethod(TypeSymbol patternType, string methodName, LookupResult lookupResult,
Copy link
Member

@agocke agocke Aug 15, 2018

Choose a reason for hiding this comment

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

I think we'll end up needing to split this out to handle different patterns differently. For instance, the GetEnumerator lookup will be like

  1. Search on type
  2. Look for conversion to IEnumerable
  3. Look for extension method

while Dispose will be like

  1. Look for conversion
  2. Look for instance method
  3. Look for extension method

I'm not sure we can really share much between these two.


//prototype:
LookupOptions lookupOptions = (searchExtensionMethods) ? LookupOptions.MustBeInvocableIfMember : LookupOptions.Default;

// Not using LookupOptions.MustBeInvocableMember because we don't want the corresponding lookup error.
Copy link
Member

Choose a reason for hiding this comment

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

This comment seems relevant. It sounds like there may be an unwanted diagnostic added.

if (!lookupResult.IsMultiViable)
MethodSymbol patternMethod = null;

if (!lookupResult.IsMultiViable && !searchExtensionMethods)
{
ReportPatternMemberLookupDiagnostics(lookupResult, patternType, methodName, syntaxExpr, warningsOnly, diagnostics, messageID);
Copy link
Member

Choose a reason for hiding this comment

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

The return null on line 3231 looks suspicious -- I don't follow why the existence of a property with the Dispose name should disallow calling an identical Dispose method. Maybe this is a question we need to take to LDM

// if there are multiple extension methods found it is ambiguous which one to use
if (extLookupResult.IsMultiViable && extLookupResult.Symbols.Count > 1)
{
diagnostics.Add(ErrorCode.WRN_PatternIsAmbiguous, syntaxExpr.Location, patternType, messageID.Localize(),
Copy link
Member

Choose a reason for hiding this comment

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

Seems like the instance lookup just tries to find something with no parameters. Why can't we do that here?

This will probably break for people using both a Dispose() and a Dispose(bool) method, which is a pretty common pattern.

@@ -768,8 +768,8 @@ private bool SatisfiesGetEnumeratorPattern(ref ForEachEnumeratorInfo.Builder bui
{
LookupResult lookupResult = LookupResult.GetInstance();
MethodSymbol getEnumeratorMethod = FindPatternMethod(collectionExprType, GetEnumeratorMethodName, lookupResult,
_syntax.Expression, warningsOnly: true, diagnostics: diagnostics,
_syntax.SyntaxTree, MessageID.IDS_Collection);
_syntax.Expression, searchExtensionMethods:false, warningsOnly: true, diagnostics: diagnostics,
Copy link
Member

Choose a reason for hiding this comment

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

Parameters on newlines should either be indented once into the new line or aligned with the first character after the opening parenthesis.

@@ -872,8 +872,7 @@ private bool SatisfiesForEachPattern(ref ForEachEnumeratorInfo.Builder builder,
lookupResult.Clear(); // Reuse the same LookupResult

MethodSymbol moveNextMethodCandidate = FindPatternMethod(enumeratorType, MoveNextMethodName, lookupResult, _syntax.Expression,
warningsOnly: false, diagnostics, _syntax.SyntaxTree, MessageID.IDS_Collection);

searchExtensionMethods:false, warningsOnly: false, diagnostics, _syntax.SyntaxTree, MessageID.IDS_Collection);
Copy link
Member

Choose a reason for hiding this comment

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

Same thing about parameter alignment.

@@ -293,7 +293,14 @@ private BoundStatement RewriteUsingStatementTryFinally(SyntaxNode syntax, BoundB

if ((!(methodOpt is null)) || Binder.TryGetSpecialTypeMember(_compilation, SpecialMember.System_IDisposable__Dispose, syntax, _diagnostics, out methodOpt))
{
disposeCall = BoundCall.Synthesized(syntax, disposedExpression, methodOpt);
if (methodOpt.IsExtensionMethod)
Copy link
Member

Choose a reason for hiding this comment

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

Consider using a ? : operator here. If it extends to multiple lines, our formatting rule is each token on a new line e.g.,

var expr = cond
    ? true-consequence
    : false-consequence;

if (patternMethod == null && searchExtensionMethods)
{
var extLookupResult = LookupResult.GetInstance();
this.LookupExtensionMethods(extLookupResult, methodName, 0, lookupOptions, ref useSiteDiagnostics);
Copy link
Member

@cston cston Aug 15, 2018

Choose a reason for hiding this comment

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

LookupExtensionMethods [](start = 21, length = 22)

Extension method lookup should stop at the first scope that contains an appropriate overload, so we should call LookupExtensionMethodsInSingleBinder for each scope, stopping at the first match.

See Binder.BindExtensionMethod().

diagnostics.Add(ErrorCode.WRN_PatternIsAmbiguous, syntaxExpr.Location, patternType, messageID.Localize(),
(MethodSymbol)extLookupResult.Symbols[0], (MethodSymbol)extLookupResult.Symbols[1]);
}
else if (extLookupResult.IsSingleViable && extLookupResult.SingleSymbolOrDefault.GetParameters().Length == 1 && extLookupResult.SingleSymbolOrDefault is MethodSymbol method)
Copy link
Member

@cston cston Aug 15, 2018

Choose a reason for hiding this comment

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

extLookupResult.SingleSymbolOrDefault is MethodSymbol method [](start = 128, length = 60)

Consider moving this expression before the previous expression so that method can be used in that expression.

And use method.Parameters rather than GetParameters().

Copy link
Member

Choose a reason for hiding this comment

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

What is the difference between the two?

Copy link
Member

Choose a reason for hiding this comment

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

GetParameters() is an extension method on Symbol which calls MethodSymbol.Parameters if this is a method. If we already know we have a method, we should use Parameters directly.


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

if (extLookupResult.IsMultiViable && extLookupResult.Symbols.Count > 1)
{
diagnostics.Add(ErrorCode.WRN_PatternIsAmbiguous, syntaxExpr.Location, patternType, messageID.Localize(),
(MethodSymbol)extLookupResult.Symbols[0], (MethodSymbol)extLookupResult.Symbols[1]);
Copy link
Member

Choose a reason for hiding this comment

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

(MethodSymbol) [](start = 22, length = 14)

Casts are not needed.

@chsienki chsienki added PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. and removed PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. labels Sep 11, 2018
@chsienki
Copy link
Contributor Author

@dotnet/roslyn-compiler Ready for second pass review, please

@chsienki chsienki force-pushed the dev/chsienki/dispose_pattern_ext_method branch from 8d309d5 to b9e6443 Compare September 20, 2018 22:05
@chsienki chsienki requested review from a team as code owners September 20, 2018 22:05
@chsienki chsienki force-pushed the dev/chsienki/dispose_pattern_ext_method branch from b9e6443 to 5c1f5fc Compare September 24, 2018 18:24
@chsienki chsienki added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Sep 24, 2018
- Add option to FindPatternMethod to allow searching for extension methods
- Generate correct call to dispose when it is an extenion method
- Add various tests
- Rename lookup pattern to strict, for the previous behavior still used by GetEnumerator/MoveNext
- Add a relaxed lookup with more relaxed semantics that will keep searching, even if it finds an invalid candidate
- Add a lookup extension method that can be used to resolve patterns via extension methods
- Update TryFindDisposePattern to search first via relaxed lookup, then extension, handling the diagnostics appropriately
- Address previous PR comments
}
}

if (candidateMethods.Count > 1)
Copy link
Member

Choose a reason for hiding this comment

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

if (candidateMethods.Count > 1) [](start = 20, length = 31)

Are we checking the this argument type? It seems reasonable to have multiple Dispose extension methods in the same scope, with different this arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, that's a great point. We're not, and the case you suggested fails. I've added the check that it's a valid receiver method and some tests to ensure we correctly handle that behaviour.

foreach (var symbol in lookupResult.Symbols)
{
//PROTOTYPE: for now, just check for the single this param. We probably want to also allow e.g. dispose(this, int a = 4, params object[] args)
if (symbol is MethodSymbol method && method.ParameterCount == 1 && !(method.ReduceExtensionMethod(patternType) 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.

ReduceExtensionMethod [](start = 100, length = 21)

It looks like this relies on ReducedExtensionMethodSymbol.Create. Consider replicating two interesting calls from that method here. Specifically:

method = method.InferExtensionMethodTypeArguments(
    patternType, Compilation, ref useSiteDiagnostics);
if ((object)method != null &&
    Conversions.ConvertExtensionMethodThis(
        method.Parameters[0].Type, patternType, ref useSiteDiagnostics))
{
    ...
}

There are a couple of reasons: 1. We probably want to return the constructed method rather than the original definition if the method is generic. 2. We should include the method in the candidate set even if there are use-site errors.

}

[Fact]
public void UsingPatternWithLessDerivedTarget()
Copy link
Member

Choose a reason for hiding this comment

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

UsingPatternWithLessDerivedTarget [](start = 20, length = 33)

Please add a test with Dispose extension methods for both C1 and C2 to verify overload resolution when calling with an instance of each.

Copy link
Member

Choose a reason for hiding this comment

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

Please also test a mix of generic and non-generic methods:

static void Dispose(this C1 c) { }
static void Dispose<T>(this T t) where T : C2 { }

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

Copy link
Member

Choose a reason for hiding this comment

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

Other variants:

static void Dispose(in SomeStruct) 
static void Dispose(ref SomeStruct)

In reply to: 221754221 [](ancestors = 221754221,221748751)

@AlekseyTs
Copy link
Contributor

It feels like this change requires changes to IOpertion and Control Flow Graph generation

- Remove the custom lookup code, and just attempt to bind using BindExtensionMethod
- Add more tests
@chsienki
Copy link
Contributor Author

chsienki commented Oct 2, 2018

I've updated the logic in FindPatternExtensionMethod to use BindExtensionMethod: this gives us all the wanted behaviour without needing to re-implement it a second time.; we do something similar during deconstruct pattern lookup.

This branch has been trying to share the lookup logic of the dispose pattern with the GetEnumerator pattern, but they're actually fairly different. I think it would make sense to undo that, and instead base the Dispose lookup on the Deconstruct pattern. Rather than trying to do that here, I'd like to get this PR in, and we can re-factor out in a separate second pass.

/// <param name="diagnostics">Populated with binding diagnostics.</param>
/// <param name="messageID">The ID of the message to use when reporting diagnostics</param>
/// <returns>The desired method or null.</returns>
private MethodSymbol FindPatternExtensionMethod(BoundExpression typeExpr, string methodName, LookupResult lookupResult,
Copy link
Member

Choose a reason for hiding this comment

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

LookupResult lookupResult [](start = 101, length = 25)

Not used.

}
}";
var compilation = CreateCompilation(source).VerifyDiagnostics();
}
Copy link
Member

Choose a reason for hiding this comment

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

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

Consider testing Dispose(this object o, params object[] args)

}
}";
var compilation = CreateCompilation(source).VerifyDiagnostics();
}
Copy link
Member

Choose a reason for hiding this comment

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

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

Consider checking IL of Main or some verification of overload resolution.

- Remove unused parameter
- Verify IL of overloads to ensure we're calling the correct methods
- Add some extra tests of interesting behavior
ImmutableArray<TypeSymbol> typeArguments = default;
MethodSymbol patternMethod = null;

var resolution = BindExtensionMethod(syntaxExpr, WellKnownMemberNames.DisposeMethodName, arguments, typeExpr, typeArguments, isMethodGroupConversion: false, returnRefKind: RefKind.None, returnType: null);
Copy link
Member

Choose a reason for hiding this comment

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

WellKnownMemberNames.DisposeMethodName [](start = 61, length = 38)

methodName

ImmutableArray<TypeSymbol> typeArguments = default;
MethodSymbol patternMethod = null;

var resolution = BindExtensionMethod(syntaxExpr, WellKnownMemberNames.DisposeMethodName, arguments, typeExpr, typeArguments, isMethodGroupConversion: false, returnRefKind: RefKind.None, returnType: null);
Copy link
Member

Choose a reason for hiding this comment

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

typeArguments [](start = 122, length = 13)

Consider using typeArguments: default rather than creating a local.

@@ -293,7 +293,9 @@ private BoundStatement RewriteUsingStatementTryFinally(SyntaxNode syntax, BoundB

if ((!(methodOpt is null)) || Binder.TryGetSpecialTypeMember(_compilation, SpecialMember.System_IDisposable__Dispose, syntax, _diagnostics, out methodOpt))
{
disposeCall = BoundCall.Synthesized(syntax, disposedExpression, methodOpt);
disposeCall = methodOpt.IsExtensionMethod
? BoundCall.Synthesized(syntax, disposedExpression, methodOpt, local)
Copy link
Member

Choose a reason for hiding this comment

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

disposedExpression [](start = 52, length = 18)

null

// using (C2 c = new C2())
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "C2 c = new C2()").WithArguments("C2").WithLocation(22, 16)
);
}
Copy link
Member

Choose a reason for hiding this comment

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

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

Consider testing with inaccessible instance method and accessible extension method.

- Don't pass receiver to synthesized bound node in extension case
- Don't hard code dispose method name in find pattern extension method
- Add test for inaccesible instance method & public extension method
}
}
}";
CompileAndVerify(source).VerifyIL("C3.Main()", @"
Copy link
Member

Choose a reason for hiding this comment

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

CompileAndVerify [](start = 12, length = 16)

Please include expectedOutput: "..." and include Console.WriteLine(...) in Dispose, to verify execution.

 - ExpectedOutput tests the same thing in a clearer way
 - CodeGenTests are testing the correct IL is emitted
@chsienki chsienki merged commit 244e01c into dotnet:features/enhanced-using Oct 4, 2018
@chsienki chsienki deleted the dev/chsienki/dispose_pattern_ext_method branch October 4, 2018 21:28
chsienki added a commit to chsienki/roslyn that referenced this pull request Nov 8, 2018
…29307)

* Allow dispose pattern to be implemented via extension method:
- Split pattern lookups into strict, relaxed and extension paths
- Use relaxed, followed by extension for dispose lookup
- Generate correct call to dispose when it is an extension method
- Add 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

6 participants