-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Pattern Using #27729
Pattern Using #27729
Conversation
MessageID messageID; | ||
switch (methodName) | ||
{ | ||
case WellKnownMemberNames.GetDisposeMethodName: |
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.
I think this should just be called DisposeMethodName
since the pattern seems to be to use the exact name of the method.
} | ||
result = null; | ||
} | ||
else if (messageID == MessageID.IDS_Disposable && !result.ReturnsVoid && warningsOnly) |
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.
How is this if
statement different from the above? It seems like more of this code could be shared and it would prevent us fram having to switch on the messageID
, which is a bit of a code smell. Seems like either this code is similar enough that we could get more sharing, or it's different enough that we might as well give up on sharing move this check elsewhere.
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.
Will move these to the individual Satisfies___Pattern
methods in UsingStatementBinder.cs and ForEachLoopBinder.cs
{ | ||
lookupResult.Clear(); | ||
|
||
HashSet<DiagnosticInfo> useSiteDiagnostics = 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.
Looks like indentation got screwed up here.
@@ -82,7 +81,28 @@ internal override BoundStatement BindUsingStatementParts(DiagnosticBag diagnosti | |||
if (!iDisposableConversion.IsImplicit) | |||
{ | |||
TypeSymbol expressionType = expressionOpt.Type; | |||
if ((object)expressionType == null || !expressionType.IsErrorType()) | |||
MethodSymbol disposeMethod = expressionType == null ? null : SatisfiesDisposePattern(expressionOpt.Type, 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.
Seems like this code should be outside the iDisposable.IsImplicit
check. That is, we should do the pattern check before the conversion is classified, in case we don't need to do a conversion at all.
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.
Doesn't look like this has been addressed.
/// <param name="exprType">Type of the expression over which to iterate</param> | ||
/// <param name="diagnostics">Populated with warnings if there are near misses</param> | ||
/// <returns>True if a matching method is found (still need to verify return type).</returns> | ||
private MethodSymbol SatisfiesDisposePattern(TypeSymbol exprType, DiagnosticBag 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.
I think this method should take the method from FindPatternMethod
as a parameter and return a bool
to indicate if it matches. Alternatively, it should probably be renamed TryFindDisposeMethod
.
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.
Can I change the return type to a bool
and add an out
parameter for DisposeMethod?
MethodSymbol disposeMethod = expressionType == null ? null : SatisfiesDisposePattern(expressionOpt.Type, diagnostics); | ||
if ((object)disposeMethod != null) | ||
{ | ||
if (!disposeMethod.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.
This seems like a check for SatisfiesDisposePattern
. If the signature didn't match, how did it satisfy the check?
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.
Or this.
@@ -132,9 +175,27 @@ internal override BoundStatement BindUsingStatementParts(DiagnosticBag diagnosti | |||
expressionOpt, | |||
iDisposableConversion, | |||
boundBody, | |||
null, // This ensures that a pattern-matched Dispose statement is not used. |
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.
Use a named argument, like methodOpt: null,
for boolean literals and null
.
@@ -934,6 +934,7 @@ | |||
<Field Name="ExpressionOpt" Type="BoundExpression" Null="allow"/> | |||
<Field Name="IDisposableConversion" Type="Conversion" /> | |||
<Field Name="Body" Type="BoundStatement"/> | |||
<Field Name="MethodOpt" Type="MethodSymbol" Null="allow"/> |
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.
Maybe DisposeMethodOpt
?
@@ -292,7 +292,11 @@ private BoundStatement RewriteUsingStatementTryFinally(SyntaxNode syntax, BoundB | |||
BoundExpression disposeCall; | |||
|
|||
MethodSymbol disposeMethodSymbol; | |||
if (Binder.TryGetSpecialTypeMember(_compilation, SpecialMember.System_IDisposable__Dispose, syntax, _diagnostics, out disposeMethodSymbol)) | |||
if (methodOpt != 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.
Cast to object or use !(methodOpt is null)
IL_0008: ldloc.0 | ||
IL_0009: brfalse.s IL_0011 | ||
IL_000b: ldloc.0 | ||
IL_000c: callvirt ""void C1.Dispose()"" |
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.
This seems weird to me. I could maybe understand a struct constrained callvirt, but this is a regular class. I don't understand why this isn't a call
. Can you look at EmitCallExpression
in the debugger for this test and see why it's choosing to use CallKind.CallVirt
instead of CallKind.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.
Actually, I think I understand this. I bet that we don't exactly know that the local is a temp for the object creation expression, so we're doing a null test. It would be good to confirm that CanUseCallOnRefTypeReceiver
is returning false
here.
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.
I'd spoken briefly with Jared about this earlier, who said that callvirt
was the correct option. A bit confused as to which I should stick with.
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.
Yeah, if there's any work to do here it's probably not part of this feature. I think we could get cleverer with some codegen, but let's not do that now.
test windows_debug_unit32_prtest please |
iDisposableConversion = originalBinder.Conversions.ClassifyImplicitConversionFromExpression(expressionOpt, iDisposable, ref useSiteDiagnostics); | ||
diagnostics.Add(expressionSyntax, useSiteDiagnostics); | ||
|
||
if (!iDisposableConversion.IsImplicit) | ||
{ | ||
TypeSymbol expressionType = expressionOpt.Type; | ||
if ((object)expressionType == null || !expressionType.IsErrorType()) | ||
if ((object)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.
This entire if
statement can be outside the top-level if
. We don't need to do the conversion if we find a successful pattern.
if ((object)expressionType == null || !expressionType.IsErrorType()) | ||
if ((object)disposeMethod != null) | ||
{ | ||
if (!disposeMethod.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.
This looks like it's checked in TryDisposePattern
. This is probably dead code.
@@ -76,13 +75,34 @@ internal override BoundStatement BindUsingStatementParts(DiagnosticBag diagnosti | |||
expressionOpt = this.BindTargetExpression(diagnostics, originalBinder); | |||
|
|||
HashSet<DiagnosticInfo> useSiteDiagnostics = null; | |||
TypeSymbol expressionType = expressionOpt.Type; | |||
MethodSymbol disposeMethod = expressionType == null ? null : TryFindDisposePattern(expressionOpt.Type, 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.
I would call this TryGetDisposePatternMethod
since we're actually trying to get a specific method, not a pattern.
if (declType.IsDynamic()) | ||
{ | ||
iDisposableConversion = Conversion.ImplicitDynamic; | ||
} | ||
else if ((object)disposeMethod != null) | ||
{ | ||
BoundStatement boundBody2 = originalBinder.BindPossibleEmbeddedStatement(_syntax.Statement, 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.
This code, the code above, and the last return look identical. This could all probably be shared by moving the disposeMethod
variable to the top of the method and flowing the variable to the final return.
{ | ||
LookupResult lookupResult = LookupResult.GetInstance(); | ||
SyntaxNode exp = _syntax.Expression != null ? (SyntaxNode) _syntax.Expression : (SyntaxNode) _syntax.Declaration; | ||
MethodSymbol disposeMethod = FindPatternMethod(exprType, WellKnownMemberNames.DisposeMethodName, lookupResult, exp, warningsOnly: true, diagnostics: diagnostics, _syntax.SyntaxTree); |
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.
diagnostics: diagnostics
is a redundant named variable.
private MethodSymbol TryFindDisposePattern(TypeSymbol exprType, DiagnosticBag diagnostics) | ||
{ | ||
LookupResult lookupResult = LookupResult.GetInstance(); | ||
SyntaxNode exp = _syntax.Expression != null ? (SyntaxNode) _syntax.Expression : (SyntaxNode) _syntax.Declaration; |
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.
I would just call this syntax
. exp
implies it is an expression, but a declaration is not an expression.
{ | ||
disposeCall = BoundCall.Synthesized(syntax, disposedExpression, methodOpt); | ||
} | ||
else if (Binder.TryGetSpecialTypeMember(_compilation, SpecialMember.System_IDisposable__Dispose, syntax, _diagnostics, out disposeMethodSymbol)) |
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.
You could rewrite this in one if
as follows:
if (!(methodOpt is null)) || Binder.TryGetSpecialTypeMember(_compilation, SpecialMember.System_IDisposable__Dispose, syntax, _diagnostics, out methodOpt))
IL_0008: ldloc.0 | ||
IL_0009: brfalse.s IL_0011 | ||
IL_000b: ldloc.0 | ||
IL_000c: callvirt ""void C1.Dispose()"" |
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.
Yeah, if there's any work to do here it's probably not part of this feature. I think we could get cleverer with some codegen, but let's not do that now.
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "Main").WithArguments("method group")); | ||
} | ||
|
||
[Fact] | ||
public void UsingPatternTest() |
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 looks like there may be a duplicate test above that used to be failing but is now succeeding. You could probably remove a few identical tests.
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "C1 c = new C1()").WithArguments("C1").WithLocation(12, 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.
All these tests seem to test the declaration form, but we should also test the expression form, i.e. using (expr)
. You could probably test both forms in the same test, just use two using blocks.
@@ -2939,7 +2942,7 @@ A catch() block after a catch (System.Exception e) block can catch non-CLS excep | |||
<value>Anonymous methods, lambda expressions, and query expressions inside structs cannot access instance members of 'this'. Consider copying 'this' to a local variable outside the anonymous method, lambda expression or query expression and using the local instead.</value> | |||
</data> | |||
<data name="ERR_NoConvToIDisp" xml:space="preserve"> | |||
<value>'{0}': type used in a using statement must be implicitly convertible to 'System.IDisposable'</value> | |||
<value>'{0}': type used in a using statement must have a public void-returning Dispose() instance method.</value> | |||
</data> |
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.
This is not a requirement. The type could implement IDisposable
using explicit interface implementation, in which case it would not have a public method like that. Perhaps
<value>'{0}': type used in a using statement must be implicitly convertible to 'System.IDisposable' or have a public void-returning Dispose() instance method.</value>
#Resolved
switch (methodName) | ||
{ | ||
case WellKnownMemberNames.DisposeMethodName: | ||
messageID = MessageID.IDS_Disposable; |
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.
IDS_Disposable [](start = 42, length = 14)
Would it make sense for the MessageID
to be passed in? (just asking) #Resolved
{ | ||
ReportPatternWarning(diagnostics, patternType, member, syntaxExpr, messageID); | ||
} | ||
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.
return [](start = 20, length = 6)
Nit: insert blank line before this, please. #Resolved
candidateMethods.Add((MethodSymbol)member); | ||
} | ||
} | ||
MethodSymbol patternMethod = PerformPatternOverloadResolution(patternType, candidateMethods, syntaxExpr, warningsOnly, diagnostics, syntaxTree, messageID); |
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.
MethodSymbol [](start = 12, length = 12)
Nit: insert blank line before this, please. #Resolved
/// The overload resolution portion of FindForEachPatternMethod. | ||
/// </summary> | ||
private MethodSymbol PerformPatternOverloadResolution | ||
(TypeSymbol patternType, ArrayBuilder<MethodSymbol> candidateMethods, |
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)
Please put the open paren at the end of the previous line. #Resolved
{ | ||
diagnostics.Add(ErrorCode.WRN_PatternStaticOrInaccessible, syntaxExpression.Location, patternType, messageID.Localize(), result); | ||
} | ||
result = 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.
result [](start = 20, length = 6)
Nit: empty line before this, please. #Resolved
if ((object)expressionType == null || !expressionType.IsErrorType()) | ||
{ | ||
Error(diagnostics, ErrorCode.ERR_NoConvToIDisp, expressionSyntax, expressionOpt.Display); | ||
} else |
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.
The else
here looks wrong. hasErrors
should always be set to true
. The if (disposeMethod is null)
should be enough to ensure we're in an error case.
hasErrors = true; | ||
if (disposeMethod is null) | ||
{ | ||
if ((object)expressionType == null || !expressionType.IsErrorType()) |
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 should be consistent and use expressionType is null
here.
@@ -110,14 +121,20 @@ internal override BoundStatement BindUsingStatementParts(DiagnosticBag diagnosti | |||
iDisposableConversion = originalBinder.Conversions.ClassifyImplicitConversionFromType(declType, iDisposable, ref useSiteDiagnostics); | |||
diagnostics.Add(declarationSyntax, useSiteDiagnostics); | |||
|
|||
if (!iDisposableConversion.IsImplicit) | |||
if (!iDisposableConversion.IsImplicit && (object)disposeMethod 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.
If you use is
the (object)
cast is unnecessary.
{ | ||
Error(diagnostics, ErrorCode.ERR_NoConvToIDisp, declarationSyntax, declType); | ||
} | ||
else |
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.
Else is also wrong here.
@@ -5404,7 +5404,7 @@ .maxstack 1 | |||
IL_001c: ldloc.2 | |||
IL_001d: brfalse.s IL_0026 | |||
IL_001f: ldloc.2 | |||
IL_0020: callvirt ""void System.IDisposable.Dispose()"" | |||
IL_0020: callvirt ""void System.IO.Stream.Dispose()"" |
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.
I expect all of the old tests to now have no changes.
test this please |
} | ||
|
||
/// <summary> | ||
/// The overload resolution portion of FindForEachPatternMethod. |
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.
I think this is called FindPatternMethod
now.
@@ -766,141 +767,14 @@ private ForEachEnumeratorInfo.Builder GetDefaultEnumeratorInfo(ForEachEnumerator | |||
private bool SatisfiesGetEnumeratorPattern(ref ForEachEnumeratorInfo.Builder builder, TypeSymbol collectionExprType, DiagnosticBag diagnostics) | |||
{ | |||
LookupResult lookupResult = LookupResult.GetInstance(); | |||
MethodSymbol getEnumeratorMethod = FindForEachPatternMethod(collectionExprType, GetEnumeratorMethodName, lookupResult, warningsOnly: true, diagnostics: diagnostics); | |||
MethodSymbol getEnumeratorMethod = FindPatternMethod(collectionExprType, GetEnumeratorMethodName, lookupResult, | |||
_syntax.Expression, warningsOnly: true, diagnostics: 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.
Looks like the indentation is one space off.
if (!iDisposableConversion.IsImplicit) | ||
TypeSymbol expressionType = expressionOpt.Type; | ||
|
||
if (!iDisposableConversion.IsImplicit && disposeMethod 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.
How could disposeMethod
be non-null here?
@@ -110,14 +119,17 @@ internal override BoundStatement BindUsingStatementParts(DiagnosticBag diagnosti | |||
iDisposableConversion = originalBinder.Conversions.ClassifyImplicitConversionFromType(declType, iDisposable, ref useSiteDiagnostics); | |||
diagnostics.Add(declarationSyntax, useSiteDiagnostics); | |||
|
|||
if (!iDisposableConversion.IsImplicit) | |||
if (!iDisposableConversion.IsImplicit && disposeMethod 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.
I also don't see how disposeMethod
could be null here.
hasErrors); | ||
} | ||
|
||
/// <summary> | ||
/// Checks for a Dispose method on exprType. Failing to satisfy the pattern is not an error - | ||
/// it just means we have to check for an interface instead. |
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.
I think this comment is wrong now since we check for an interface before looking for the pattern. I don't think that this code should add any errors, though. The error reporting in the other methods makes sense.
/// </summary> | ||
/// <param name="exprType">Type of the expression over which to iterate</param> | ||
/// <param name="diagnostics">Populated with warnings if there are near misses</param> | ||
/// <returns>True if a matching method is found (still need to verify return type).</returns> |
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 looks like this method verifies the return type.
LookupResult lookupResult = LookupResult.GetInstance(); | ||
SyntaxNode syntax = _syntax.Expression != null ? (SyntaxNode)_syntax.Expression : (SyntaxNode)_syntax.Declaration; | ||
MethodSymbol disposeMethod = FindPatternMethod(exprType, WellKnownMemberNames.DisposeMethodName, lookupResult, syntax, warningsOnly: true, diagnostics, _syntax.SyntaxTree, MessageID.IDS_Disposable); | ||
lookupResult.Free(); |
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.
This lookupResult
logic is crazy complicated (not your fault and not something I would necessarily fix in this PR).
// (8,16): error CS1674: 'Program.S1': type used in a using statement must be implicitly convertible to 'System.IDisposable' | ||
// using (new S1()) | ||
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "new S1()").WithArguments("Program.S1").WithLocation(8, 16) | ||
Diagnostic(ErrorCode.ERR_RefStructInterfaceImpl, "IDisposable").WithArguments("Program.S1", "System.IDisposable").WithLocation(14, 28) |
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.
Probably worth adding a test below this one which is the same except S1
doesn't implement IDisposable. It should work fine now. 😄
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.
LGTM
|
I will be championing the feature. But at the same time I don't think we need to block this PR on having a champion as it exists on a feature branch, not any shipping branch.
C# 8.0 will be my proposed release but again though not sure we need to discuss that for the purpose of this PR. Probably best to discuss that on the csharplang repository. |
OK, then I'll drop by with language questions for this feature. |
Need a link in the PR description to whatever is being used as a spec. Is it dotnet/csharplang#1623 ? |
// (8,16): error CS1674: 'Program.S1': type used in a using statement must be implicitly convertible to 'System.IDisposable' | ||
// using (new S1()) | ||
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "new S1()").WithArguments("Program.S1").WithLocation(8, 16) | ||
Diagnostic(ErrorCode.ERR_RefStructInterfaceImpl, "IDisposable").WithArguments("Program.S1", "System.IDisposable").WithLocation(14, 28) |
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_RefStructInterfaceImpl [](start = 37, length = 26)
The comment does not align with the error message here.
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. | ||
// using (C1 c = new C1()) | ||
Diagnostic(ErrorCode.ERR_NoConvToIDisp, "C1 c = new C1()").WithArguments("C1").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.
ErrorCode [](start = 27, length = 9)
Can we report only one error for this instead of two?
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.
LGTM assuming the spec is dotnet/csharplang#1623. Before integrating, please open an issue about the cascaded diagnostic in UsingPatternSameSignatureAmbiguousTest
and correct the comment on a test where noted in SpanStackSafetyTests.cs
test InterfaceImpl
.
@fayrose I marked the PR as "for personal review" since it is not actively being reviewed. Please remove the label once you've added the tests. Thanks |
Sounds good. Ready to merge then? |
Description
A user has a class method Dispose() but has not or does not want to implement the IDisposable interface. This allows the user to simply implement a public void-returning instance method called Dispose that bypasses the official interface check and thus allows this type to be utilized in a using statement.
An outline of a spec is in dotnet/csharplang#1623