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

Diagnostic Improvements for error #0281 #27052

Merged
merged 23 commits into from
Jun 2, 2018
Merged

Conversation

fayrose
Copy link
Contributor

@fayrose fayrose commented May 22, 2018

Customer scenario

What does the customer do to get into this situation, and why do we think this
is common enough to address for this release. (Granted, sometimes this will be
obvious "Open project, VS crashes" but in general, I need to understand how
common a scenario is)

When one tries to access a public method in an internal class, or when one tries to access an internal method in a public class.

Bugs this fixes

#17322

Workarounds, if any

No, diagnostic improvement.

Risk

Very low.

Performance impact

Negligible: differentiates between two error cases but does not impact performance.

Is this a regression from a previous update?

Yes, regression from native compiler.

Root cause analysis

How did we miss it? What tests are we adding to guard against it in the future?
Insufficient test coverage.

How was the bug found?

(E.g. customer reported it vs. ad hoc testing)

Marek Safar - external-ish.

Test documentation updated?

No, diagnostic improvement.

If this is a new non-compiler feature or a significant improvement to an existing feature, update https://github.com/dotnet/roslyn/wiki/Manual-Testing once you know which release it is targeting.

@fayrose fayrose requested a review from a team as a code owner May 22, 2018 19:51
@fayrose fayrose requested a review from agocke May 22, 2018 19:51
@agocke
Copy link
Member

agocke commented May 22, 2018

Looks like the bootstrap is failing. Build.cmd -bootstrap should replicate the failure.

@@ -1909,7 +1909,7 @@ private void CheckOptimisticIVTAccessGrants(DiagnosticBag bag)

if (conclusion == IVTConclusion.PublicKeyDoesntMatch)
bag.Add(ErrorCode.ERR_FriendRefNotEqualToThis, NoLocation.Singleton,
otherAssembly.Identity);
otherAssembly.Identity.ToString(), this.ContainingAssembly.Identity.ToString());
Copy link
Member

Choose a reason for hiding this comment

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

I don't believe we should be calling otherAssembly.Identity.ToString here. That will cause it to be captured under the current UI culture. Diagnostics can have their messages realized under a different culture.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

toString() has been overridden to be culture-independent.

Copy link
Member

@cston cston May 24, 2018

Choose a reason for hiding this comment

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

Why use ToString() here rather than letting the MessageProvider serialize the arguments?

Copy link
Member

Choose a reason for hiding this comment

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

I usually prefer to be explicit on how things get serialized to strings -- Jared's comment reflects that this is usually a very deliberate decision and being explicit signals intent better, IMHO.

@@ -63,6 +64,23 @@ public static ImmutableArray<T> AsImmutableOrNull<T>(this IEnumerable<T> items)
return ImmutableArray.CreateRange<T>(items);
}

public static string PublicKeyToString(this ImmutableArray<byte> key)
Copy link
Member

Choose a reason for hiding this comment

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

Probably don't want this as a general purpose extension method on ImmutableArray<byte>. Feels pretty specific to a scenario.


foreach (ImmutableArray<byte> key in unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name))
{
friendRefNotEqualToThis = key.SequenceEqual(this.ContainingType.ContainingAssembly.Identity.PublicKey) ? false : friendRefNotEqualToThis;
Copy link
Member

Choose a reason for hiding this comment

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

indentation seems off here...

{
var unwrappedSymbols = ImmutableArray.Create<Symbol>(unwrappedSymbol);
diagInfo = diagnose ? new CSDiagnosticInfo(ErrorCode.ERR_BadAccess, new[] { unwrappedSymbol }, unwrappedSymbols, additionalLocations: ImmutableArray<Location>.Empty) : null;
}
Copy link
Member

Choose a reason for hiding this comment

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

Consider checking diagnose separately from each individual case. And perhaps combine all checks with ?::

var diagInfo = diagnose ?
    inaccessibleViaQualifier ?
        new CSDiagnosticInfo(...) :
        friendNotEqualToThis ?
            new CSDiagnosticInfo(...) :
            new CSDiagnosticInfo(...) :
        null;

foreach (ImmutableArray<byte> key in unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name))
{
friendRefNotEqualToThis = key.SequenceEqual(this.ContainingType.ContainingAssembly.Identity.PublicKey) ? false : friendRefNotEqualToThis;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can be combined with friendRefNotEqualToThis assignment above:

bool friendRefNotEqualToThis = unwrappedSymbol != null && ... &&
        unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(
            this.ContainingAssembly.Name).Any((key, publicKey) =>
                key.SequenceEqual(publicKey),
                this.ContainingAssembly.Identity.PublicKey);

Copy link
Member

Choose a reason for hiding this comment

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

That would capture this, and capturing lambdas are prohibited in compiler code.

Copy link
Member

@cston cston May 24, 2018

Choose a reason for hiding this comment

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

My mistake, there is no Any overload that takes an additional arg parameter passed to the predicate. @agocke is correct that means the lambda would need to capture a local or this.

@@ -1148,21 +1147,33 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio
return LookupResult.Inaccessible(symbol, diagInfo);
}
else if (!InCref &&
!this.IsAccessible(unwrappedSymbol,
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 old whitespace was correct

var unwrappedSymbols = ImmutableArray.Create<Symbol>(unwrappedSymbol);
diagInfo = diagnose ? new CSDiagnosticInfo(ErrorCode.ERR_BadAccess, new[] { unwrappedSymbol }, unwrappedSymbols, additionalLocations: ImmutableArray<Location>.Empty) : null;
}
bool friendRefNotEqualToThis = unwrappedSymbol != null && unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name).Any()
Copy link
Member

Choose a reason for hiding this comment

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

I don't think unwrappedSymbol can be null here. It looks like it's always assigned above.


foreach (ImmutableArray<byte> key in unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name))
{
friendRefNotEqualToThis = key.SequenceEqual(this.ContainingType.ContainingAssembly.Identity.PublicKey) ? false : friendRefNotEqualToThis;
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 move all this logic into a local function. That way if the first if statement is true, we don't waste any time doing unnecessary calculations.

@@ -1196,7 +1207,7 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio
return LookupResult.Good(symbol);
}
}

Copy link
Member

Choose a reason for hiding this comment

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

Accidental whitespace change.

}

bool friendRefNotEqualToThis = unwrappedSymbol != null && unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name).Any()
&& unwrappedSymbol.DeclaredAccessibility.Equals(Accessibility.Internal);
Copy link
Member

Choose a reason for hiding this comment

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

You should be able to use == instead of .Equals here.

diagInfo = diagnose ? new CSDiagnosticInfo(ErrorCode.ERR_BadAccess, new[] { unwrappedSymbol }, unwrappedSymbols, additionalLocations: ImmutableArray<Location>.Empty) : null;
}

bool friendRefNotEqualToThis = unwrappedSymbol != null && unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name).Any()
Copy link
Member

Choose a reason for hiding this comment

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

Can ContainingAssembly.Name be null here?

friendRefNotEqualToThis = key.SequenceEqual(this.ContainingType.ContainingAssembly.Identity.PublicKey) ? false : friendRefNotEqualToThis;
}

diagInfo = diagnose ?
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure this is what @cston had in mind. I think he just wanted the diagnose check lifted to before the first if statement, but I may be wrong. complete conversion to ? : doesn't look more readable to me.

@@ -7,6 +7,7 @@
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
Copy link
Member

Choose a reason for hiding this comment

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

Please revert the change to this file, import doesn't appear to be necessary.

@@ -58,6 +58,20 @@ public override string ToString()
return GetDisplayName(fullKey: false);
}


internal static string PublicKeyToString(ImmutableArray<byte> key)
Copy link
Member

@jaredpar jaredpar May 23, 2018

Choose a reason for hiding this comment

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

Nit: double blank line above. #Resolved

var unwrappedSymbols = ImmutableArray.Create<Symbol>(unwrappedSymbol);
diagInfo = diagnose ? new CSDiagnosticInfo(ErrorCode.ERR_BadAccess, new[] { unwrappedSymbol }, unwrappedSymbols, additionalLocations: ImmutableArray<Location>.Empty) : null;
}
bool friendRefNotEqualToThis = getFriendRefNotEqualToThis();
Copy link
Member

Choose a reason for hiding this comment

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

Calling it up front means we still pay for the execution. I would inline this call.


bool getFriendRefNotEqualToThis()
{
if (inaccessibleViaQualifier)
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 check should be outside of the local funciton.

{
Debugger.Launch();
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this catch block necessary?

{
try
{
if (inaccessibleViaQualifier)
Copy link
Member

Choose a reason for hiding this comment

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

The caller already checks this.

&& unwrappedSymbol.DeclaredAccessibility == Accessibility.Internal
&& this.Compilation.Assembly.PublicKey != null;

if (temp && unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.Compilation.AssemblyName).Any())
Copy link
Member

Choose a reason for hiding this comment

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

Is the && unwrappedSymbol...Any() check needed?

{
foreach (ImmutableArray<byte> key in unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.Compilation.AssemblyName))
{
temp = key.SequenceEqual(this.Compilation.Assembly.Identity.PublicKey) ? false : temp;
Copy link
Member

Choose a reason for hiding this comment

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

No need to check keys beyond the first match.

if (key.SequenceEqual(this.Compilation.Assembly.Identity.PublicKey))
{
    return false;
}

var friendClass = CreateCompilation(@"
using System.Runtime.CompilerServices;
[ assembly: InternalsVisibleTo(""cs0281, PublicKey=00240000048000009400000006020000002400005253413100040000010001002b986f6b5ea5717d35c72d38561f413e267029efa9b5f107b9331d83df657381325b3a67b75812f63a9436ceccb49494de8f574f8e639d4d26c0fcf8b0e9a1a196b80b6f6ed053628d10d027e032df2ed1d60835e5f47d32c9ef6da10d0366a319573362c821b5f8fa5abc5bb22241de6f666a85d82d6ba8c3090d01636bd2bb"") ]
public class FriendClass
Copy link
Member

Choose a reason for hiding this comment

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

Should the class be named PublicClass?

Diagnostic((ErrorCode)errorCode, "MyMethod").WithArguments("FriendClass.MyMethod()").WithLocation(7, 15)
);
break;
default: throw new NotImplementedException();
Copy link
Member

Choose a reason for hiding this comment

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

The InlineData makes this test difficult to read. Could this be written more explicitly and without InlineData? Perhaps:

    var source1 = @"
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo(...)]
public class PublicClass
{
    internal static void InternalMethod() { }
    protected static void ProtectedMethod() { }
    private static void PrivateMethod() { }
    internal protected static void InternalProtectedMethod() { }
    private protected static void PrivateProtectedMethod() { }
}";
    var source2 = @"
class Test
{
    static void Main()
    {
        PublicClass.InternalMethod();
        PublicClass.ProtectedMethod();
        ...
    }
}";

{
diagInfo = new CSDiagnosticInfo(ErrorCode.ERR_BadProtectedAccess, unwrappedSymbol, accessThroughType, this.ContainingType);
}
else if (getFriendRefNotEqualToThis())
Copy link
Member

Choose a reason for hiding this comment

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

We usually prefix bool-returning functions and properties with words like Is or Has. I would call this function something like isBadIvtSpecification.

// PublicClass.InternalMethod();
Diagnostic(ErrorCode.ERR_FriendRefNotEqualToThis, "InternalMethod").WithArguments("Paul, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "").WithLocation(7, 15),
// (8,21): error CS0281: Friend access was granted by 'Paul, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null', but the public key of the output assembly ('') does not match that specified by the InternalsVisibleTo attribute in the granting assembly.
// PublicClass.ProtectedMethod();
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't look right. protected members are not affected by InternalsVisibleTo, so we should not provide the new error message for protected accesses, only things that have effectively "internal" access.

);

}

Copy link
Member

Choose a reason for hiding this comment

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

Since we previously hit a bug due to accessing the symbol from a using alias, I would expect a regression test that accesses an internal symbol via a using alias, like

using MyAlias = OtherNamespace.FriendClass;

Diagnostic(ErrorCode.ERR_NoSuchMember, "PrivateMethod").WithArguments("PublicClass", "PrivateMethod").WithLocation(9, 21),
// (10,21): error CS0122: 'PublicClass.InternalProtectedMethod()' is inaccessible due to its protection level
// PublicClass.InternalProtectedMethod();
Diagnostic(ErrorCode.ERR_BadAccess, "InternalProtectedMethod").WithArguments("PublicClass.InternalProtectedMethod()").WithLocation(10, 21),
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 these two are the wrong message. "protected internal" and "private protected" should be the same as "internal"

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 assuming tests pass and minor comments are addressed


bool IsBadIvtSpecification()
{
if ((unwrappedSymbol.DeclaredAccessibility == Accessibility.Internal || unwrappedSymbol.DeclaredAccessibility == Accessibility.ProtectedAndInternal
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 split these each onto their own line for readability, i.e.

(unwrappedSymbol.DeclaredAccessibility == Accessibility.Internal || 
 unwrappedSymbol.DeclaredAccessibility == Accessibility.ProtectedAndInternal ||
 unwrappedSymbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal)

if ((unwrappedSymbol.DeclaredAccessibility == Accessibility.Internal || unwrappedSymbol.DeclaredAccessibility == Accessibility.ProtectedAndInternal
|| unwrappedSymbol.DeclaredAccessibility == Accessibility.ProtectedOrInternal)
// Ensures that during binding we don't ask for public key which results in attribute binding and stack overflow.
// If looking up attributes, don't ask for public key.
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 these comment lines are out of order (it reads better if the lower one is at the top).

@agocke
Copy link
Member

agocke commented May 30, 2018

Looks like there are a few new broken tests that just need rebaselining.

@fayrose fayrose requested a review from a team as a code owner May 30, 2018 21:44
@jaredpar
Copy link
Member

retest this please

@mmitche
Copy link
Member

mmitche commented May 31, 2018

@dotnet-bot test windows_debug_spanish_unit32_prtest

return LookupResult.Inaccessible(symbol, diagInfo);
if (!diagnose)
{
diagInfo = null;
Copy link
Member

@jaredpar jaredpar May 31, 2018

Choose a reason for hiding this comment

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

Indent still seems off here. Currently three spaces, should be four.

}
else if (IsBadIvtSpecification())
{
diagInfo = new CSDiagnosticInfo(ErrorCode.ERR_FriendRefNotEqualToThis, unwrappedSymbol.ContainingAssembly.Identity.ToString(), AssemblyIdentity.PublicKeyToString(this.Compilation.Assembly.PublicKey));
Copy link
Member

Choose a reason for hiding this comment

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

Indent is off here. Currently seven spaces, should be four.,

}
else if (inaccessibleViaQualifier)
{
diagInfo = new CSDiagnosticInfo(ErrorCode.ERR_BadProtectedAccess, unwrappedSymbol, accessThroughType, this.ContainingType);
Copy link
Member

Choose a reason for hiding this comment

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

Indent is off here. currently six spaces, should be four.

Copy link
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

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

Couple minor style issues but otherwise looks good.

.VerifyDiagnostics(
// (12,11): error CS0122: 'Base.Field1' is inaccessible due to its protection level

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 wrong. There are no IVT attributes or references in this compilation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

b.Field1 is trying to access a private protected field it has no access to. What error should be thrown instead?

Copy link
Member

Choose a reason for hiding this comment

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

The old message looks correct here. The current one doesn't make any sense. How could a compilation provide IVT access to itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok will change it back. The test passes only with the new error. How should we differentiate this from other cases?

return false;
}
}
return true;
Copy link
Member

Choose a reason for hiding this comment

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

This returns true even if there are no IVT attributes. The check should be that there is at least one attribute, but all of the public keys are wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

line 1221 is nested within the greater Accessibility check statement, and is only called if all of the SequenceEquals have returned false / the public keys are wrong. There is a return false at line 1223, outside of the accessibility check. Are you referring to different attributes?

Copy link
Member

Choose a reason for hiding this comment

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

DeclaredAccessibility just means what it was declared as, e.g. internal. No check on if it has IVT access.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, then how does one test for attributes?

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 you can just check if the thing you're going to foreach over is empty before trying and return false if so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pushed

@@ -1204,14 +1204,16 @@ internal SingleLookupResult CheckViability(Symbol symbol, int arity, LookupOptio

bool IsBadIvtSpecification()
{
IEnumerable<ImmutableArray<byte>> keys = unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.Compilation.AssemblyName);
Copy link
Member

Choose a reason for hiding this comment

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

You have to do this inside the if or, as the comment says, we'll stack overflow when binding attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

pushed

@fayrose fayrose requested a review from a team as a code owner June 1, 2018 21:26
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 once unnecessary changes are reverted and tests pass.

@@ -266,7 +268,7 @@ void M()
}
}
";
CreateCompilation(source, parseOptions: TestOptions.Regular7_2)
CreateCompilation(source, parseOptions: TestOptions.Regular7_2, assemblyName: "Paul")
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 you can revert all changes to this file now.

@@ -352,7 +354,7 @@ private protected class Inner // error: protected not allowed in struct
}
}
";
CreateCompilation(source, parseOptions: TestOptions.Regular7_2)
CreateCompilation(source, parseOptions: TestOptions.Regular7_2, assemblyName: "Paul")
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

@@ -633,7 +633,7 @@ public class C
internal class D
{
static public int d_pub;
}");
}", assemblyName: "Paul");
Copy link
Member

Choose a reason for hiding this comment

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

And here.

@@ -1642,7 +1643,7 @@ public void ProtectedAndInternalNestedBaseClass()
var il = @"
.assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) }

.assembly '<<GeneratedFileName>>'
.assembly 'Paul'
Copy link
Member

Choose a reason for hiding this comment

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

And here.

@@ -145,7 +145,7 @@ System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globaliz
> System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo(""de-DE"")
> ? System.Math.PI
3,1415926535897931
>", runner.Console.Out.ToString())
> ", runner.Console.Out.ToString())
Copy link
Member

Choose a reason for hiding this comment

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

And here.

@fayrose fayrose merged commit 1e8ba78 into dotnet:master Jun 2, 2018
@fayrose fayrose deleted the larose-cs0281 branch June 2, 2018 07:05
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.

8 participants