Diagnostic Improvements for error #0281#27052
Conversation
|
Looks like the bootstrap is failing. |
| if (conclusion == IVTConclusion.PublicKeyDoesntMatch) | ||
| bag.Add(ErrorCode.ERR_FriendRefNotEqualToThis, NoLocation.Singleton, | ||
| otherAssembly.Identity); | ||
| otherAssembly.Identity.ToString(), this.ContainingAssembly.Identity.ToString()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
toString() has been overridden to be culture-independent.
There was a problem hiding this comment.
Why use ToString() here rather than letting the MessageProvider serialize the arguments?
There was a problem hiding this comment.
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.
| return ImmutableArray.CreateRange<T>(items); | ||
| } | ||
|
|
||
| public static string PublicKeyToString(this ImmutableArray<byte> key) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; | ||
| } |
There was a problem hiding this comment.
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; | ||
| } |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
That would capture this, and capturing lambdas are prohibited in compiler code.
There was a problem hiding this comment.
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.
| return LookupResult.Inaccessible(symbol, diagInfo); | ||
| } | ||
| else if (!InCref && | ||
| !this.IsAccessible(unwrappedSymbol, |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| bool friendRefNotEqualToThis = unwrappedSymbol != null && unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.ContainingType.ContainingAssembly.Name).Any() | ||
| && unwrappedSymbol.DeclaredAccessibility.Equals(Accessibility.Internal); |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
Can ContainingAssembly.Name be null here?
| friendRefNotEqualToThis = key.SequenceEqual(this.ContainingType.ContainingAssembly.Identity.PublicKey) ? false : friendRefNotEqualToThis; | ||
| } | ||
|
|
||
| diagInfo = diagnose ? |
There was a problem hiding this comment.
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.
| using System.IO; | ||
| using System.Linq; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Text; |
There was a problem hiding this comment.
Please revert the change to this file, import doesn't appear to be necessary.
| } | ||
|
|
||
|
|
||
| internal static string PublicKeyToString(ImmutableArray<byte> key) |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
Calling it up front means we still pay for the execution. I would inline this call.
|
|
||
| bool getFriendRefNotEqualToThis() | ||
| { | ||
| if (inaccessibleViaQualifier) |
There was a problem hiding this comment.
I think this check should be outside of the local funciton.
| { | ||
| Debugger.Launch(); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Is this catch block necessary?
| { | ||
| try | ||
| { | ||
| if (inaccessibleViaQualifier) |
There was a problem hiding this comment.
The caller already checks this.
| && unwrappedSymbol.DeclaredAccessibility == Accessibility.Internal | ||
| && this.Compilation.Assembly.PublicKey != null; | ||
|
|
||
| if (temp && unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.Compilation.AssemblyName).Any()) |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Should the class be named PublicClass?
| Diagnostic((ErrorCode)errorCode, "MyMethod").WithArguments("FriendClass.MyMethod()").WithLocation(7, 15) | ||
| ); | ||
| break; | ||
| default: throw new NotImplementedException(); |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
| ); | ||
|
|
||
| } | ||
|
|
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
I think these two are the wrong message. "protected internal" and "private protected" should be the same as "internal"
agocke
left a comment
There was a problem hiding this comment.
LGTM assuming tests pass and minor comments are addressed
|
|
||
| bool IsBadIvtSpecification() | ||
| { | ||
| if ((unwrappedSymbol.DeclaredAccessibility == Accessibility.Internal || unwrappedSymbol.DeclaredAccessibility == Accessibility.ProtectedAndInternal |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
I think these comment lines are out of order (it reads better if the lower one is at the top).
|
Looks like there are a few new broken tests that just need rebaselining. |
|
retest this please |
|
@dotnet-bot test windows_debug_spanish_unit32_prtest |
| return LookupResult.Inaccessible(symbol, diagInfo); | ||
| if (!diagnose) | ||
| { | ||
| diagInfo = null; |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
Indent is off here. Currently seven spaces, should be four.,
| } | ||
| else if (inaccessibleViaQualifier) | ||
| { | ||
| diagInfo = new CSDiagnosticInfo(ErrorCode.ERR_BadProtectedAccess, unwrappedSymbol, accessThroughType, this.ContainingType); |
There was a problem hiding this comment.
Indent is off here. currently six spaces, should be four.
jaredpar
left a comment
There was a problem hiding this comment.
Couple minor style issues but otherwise looks good.
| CreateCompilation(source, parseOptions: TestOptions.Regular7_2, assemblyName: "Paul") | ||
| .VerifyDiagnostics( | ||
| // (12,11): error CS0122: 'Base.Field1' is inaccessible due to its protection level | ||
|
|
There was a problem hiding this comment.
This looks wrong. There are no IVT attributes or references in this compilation.
There was a problem hiding this comment.
b.Field1 is trying to access a private protected field it has no access to. What error should be thrown instead?
There was a problem hiding this comment.
The old message looks correct here. The current one doesn't make any sense. How could a compilation provide IVT access to itself?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
DeclaredAccessibility just means what it was declared as, e.g. internal. No check on if it has IVT access.
There was a problem hiding this comment.
Ah, then how does one test for attributes?
There was a problem hiding this comment.
I think you can just check if the thing you're going to foreach over is empty before trying and return false if so.
|
|
||
| bool IsBadIvtSpecification() | ||
| { | ||
| IEnumerable<ImmutableArray<byte>> keys = unwrappedSymbol.ContainingAssembly.GetInternalsVisibleToPublicKeys(this.Compilation.AssemblyName); |
There was a problem hiding this comment.
You have to do this inside the if or, as the comment says, we'll stack overflow when binding attributes.
| } | ||
| "; | ||
| CreateCompilation(source, parseOptions: TestOptions.Regular7_2) | ||
| CreateCompilation(source, parseOptions: TestOptions.Regular7_2, assemblyName: "Paul") |
There was a problem hiding this comment.
I think you can revert all changes to this file now.
| } | ||
| "; | ||
| CreateCompilation(source, parseOptions: TestOptions.Regular7_2) | ||
| CreateCompilation(source, parseOptions: TestOptions.Regular7_2, assemblyName: "Paul") |
| { | ||
| static public int d_pub; | ||
| }"); | ||
| }", assemblyName: "Paul"); |
| .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } | ||
|
|
||
| .assembly '<<GeneratedFileName>>' | ||
| .assembly 'Paul' |
| > ? System.Math.PI | ||
| 3,1415926535897931 | ||
| >", runner.Console.Out.ToString()) | ||
| > ", runner.Console.Out.ToString()) |
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.