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

Pattern Using #27729

Merged
merged 24 commits into from Jun 28, 2018
Merged

Pattern Using #27729

merged 24 commits into from Jun 28, 2018

Conversation

fayrose
Copy link
Contributor

@fayrose fayrose commented Jun 11, 2018

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

@fayrose fayrose requested a review from a team as a code owner June 11, 2018 20:22
@fayrose fayrose requested a review from agocke June 11, 2018 20:22
MessageID messageID;
switch (methodName)
{
case WellKnownMemberNames.GetDisposeMethodName:
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 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)
Copy link
Member

@agocke agocke Jun 11, 2018

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.

Copy link
Contributor Author

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

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

Copy link
Member

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

Copy link
Contributor Author

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

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?

Copy link
Member

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

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

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

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

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?

Copy link
Member

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.

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

Copy link
Member

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.

@gafter gafter self-requested a review June 12, 2018 00:17
@fayrose
Copy link
Contributor Author

fayrose commented Jun 12, 2018

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

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

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

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

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

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

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

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

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

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

Copy link
Member

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.

@gafter gafter added this to Next Up in Compiler: Gafter Jun 12, 2018
@@ -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>
Copy link
Member

@gafter gafter Jun 12, 2018

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

@gafter gafter Jun 12, 2018

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

@gafter gafter Jun 12, 2018

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

@gafter gafter Jun 12, 2018

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

@gafter gafter Jun 12, 2018

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

@gafter gafter Jun 12, 2018

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

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

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

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

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

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.

@gafter gafter removed this from Next Up in Compiler: Gafter Jun 12, 2018
@fayrose
Copy link
Contributor Author

fayrose commented Jun 13, 2018

test this please

}

/// <summary>
/// The overload resolution portion of FindForEachPatternMethod.
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 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,
Copy link
Member

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

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

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

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

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

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

Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

LGTM

@gafter
Copy link
Member

gafter commented Jun 18, 2018

  • Is there an LDM champion for this feature? Link to the championed feature issue please.
  • Is there a draft specification for this feature? Link to the draft spec please.
  • What is the tentative proposed language milestone for it?

@jaredpar
Copy link
Member

@gafter

Is there an LDM champion for this feature? Link to the championed feature issue please.

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.

What is the tentative proposed language milestone for it?

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.

@gafter
Copy link
Member

gafter commented Jun 18, 2018

I will be championing the feature.

OK, then I'll drop by with language questions for this feature.

@gafter
Copy link
Member

gafter commented Jun 18, 2018

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

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

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?

Copy link
Member

@gafter gafter left a 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.

@agocke agocke requested review from jaredpar and a team June 20, 2018 20:18
@jcouv jcouv added this to the 16.0 milestone Jun 27, 2018
@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jun 27, 2018
@jcouv
Copy link
Member

jcouv commented Jun 27, 2018

@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

@jaredpar
Copy link
Member

@jcouv spoke with @fayrose earlier today. The POR is to get this PR merged and follow up with another PR for the additional tests I requested.

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

jcouv commented Jun 27, 2018

Sounds good. Ready to merge then?

@fayrose fayrose merged commit 336e138 into dotnet:features/enhanced-using Jun 28, 2018
@fayrose fayrose deleted the pattern-using branch June 28, 2018 20:08
@fayrose fayrose restored the pattern-using branch June 28, 2018 20:08
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

5 participants