Skip to content

Commit f920262

Browse files
authored
Add the BestFriend attribute for restricting cross-assembly internal access (#1520)
* Add the `BestFriend` attribute for restricting cross-assembly internal access.
1 parent dcc4e0e commit f920262

File tree

8 files changed

+371
-31
lines changed

8 files changed

+371
-31
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
7+
namespace Microsoft.ML
8+
{
9+
/// <summary>
10+
/// Intended to be applied to types and members marked as internal to indicate that friend access of this
11+
/// internal item is OK from another assembly. This restriction applies only to assemblies that declare the
12+
/// <see cref="WantsToBeBestFriendsAttribute"/> assembly level attribute. Note that this attribute is not
13+
/// transferrable: an internal member with this attribute does not somehow make a containing internal type
14+
/// accessible. Conversely, neither does marking an internal type make any unmarked internal members accessible.
15+
/// </summary>
16+
[BestFriend]
17+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Constructor
18+
| AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Enum | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
19+
internal sealed class BestFriendAttribute : Attribute
20+
{
21+
}
22+
23+
/// <summary>
24+
/// This is an assembly level attribute to signal that friend accesses on this assembly should be checked
25+
/// for usage of <see cref="BestFriendAttribute"/>. If this attribute is missing, normal access rules for
26+
/// friends should apply.
27+
/// </summary>
28+
[BestFriend]
29+
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
30+
internal sealed class WantsToBeBestFriendsAttribute : Attribute
31+
{
32+
}
33+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CSharp;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.ML.CodeAnalyzer.Tests.Helpers;
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Collections.Immutable;
12+
using System.IO;
13+
using System.Linq;
14+
using System.Reflection;
15+
using Xunit;
16+
17+
namespace Microsoft.ML.InternalCodeAnalyzer.Tests
18+
{
19+
public sealed class BestFriendTest : DiagnosticVerifier<BestFriendAnalyzer>
20+
{
21+
// We do things in this somewhat odd way rather than just referencing the Core assembly directly,
22+
// because we certainly want the best friend attribute itself to be internal, but the assembly
23+
// we build dynamically as part of the test cannot be signed, and so cannot itself be a friend
24+
// of the core assembly (even if we were in a mood to pollute the core assembly with friend
25+
// declarations to enable this one test). We instead compile the same source, as part of this
26+
// dummy assembly. The type name will be the same so the same analyzer will work.
27+
private readonly Lazy<string> SourceAttribute = TestUtils.LazySource("BestFriendAttribute.cs");
28+
private readonly Lazy<string> SourceDeclaration = TestUtils.LazySource("BestFriendDeclaration.cs");
29+
private readonly Lazy<string> SourceUser = TestUtils.LazySource("BestFriendUser.cs");
30+
31+
[Fact]
32+
public void BestFriend()
33+
{
34+
// The setup to this one is a bit more involved than many of the analyzer tests,
35+
// because in this case we have to actually set up *two* assemblies, where the
36+
// first considers the second a friend. But, setting up this dependency structure
37+
// so that things actually compile to the point where the analyzer can actually do
38+
// its work is rather involved.
39+
Solution solution = null;
40+
var projA = CreateProject("ProjectA", ref solution, SourceDeclaration.Value);
41+
var projB = CreateProject("ProjectB", ref solution, SourceUser.Value);
42+
solution = solution.AddProjectReference(projB.Id, new ProjectReference(projA.Id));
43+
44+
var analyzer = new BestFriendAnalyzer();
45+
46+
MetadataReference peRef;
47+
var refs = new[] {
48+
RefFromType<object>(), RefFromType<Attribute>(),
49+
MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0").Location),
50+
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime, Version=0.0.0.0").Location)
51+
};
52+
using (var ms = new MemoryStream())
53+
{
54+
// We also test whether private protected can be accessed, so we need C# 7.2 at least.
55+
var parseOpts = new CSharpParseOptions(LanguageVersion.CSharp7_3);
56+
var tree = CSharpSyntaxTree.ParseText(SourceDeclaration.Value, parseOpts);
57+
var treeAttr = CSharpSyntaxTree.ParseText(SourceAttribute.Value, parseOpts);
58+
59+
var compOpts = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
60+
var innerComp = CSharpCompilation.Create(projA.Name, new[] { tree, treeAttr }, refs, compOpts);
61+
62+
var emitResult = innerComp.Emit(ms);
63+
Assert.True(emitResult.Success, $"Compilation of {projA.Name} did not work. Diagnostics: {string.Join(" || ", emitResult.Diagnostics)}");
64+
65+
var peImage = ms.ToArray().ToImmutableArray();
66+
peRef = MetadataReference.CreateFromImage(peImage);
67+
}
68+
69+
var comp = projB.GetCompilationAsync().Result
70+
.WithReferences(refs.Append(peRef).ToArray());
71+
var compilationWithAnalyzers = comp.WithAnalyzers(ImmutableArray.Create((DiagnosticAnalyzer)analyzer));
72+
var allDiags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
73+
74+
var projectTrees = new HashSet<SyntaxTree>(projB.Documents.Select(r => r.GetSyntaxTreeAsync().Result));
75+
var diags = allDiags
76+
.Where(d => d.Location == Location.None || d.Location.IsInMetadata || projectTrees.Contains(d.Location.SourceTree))
77+
.OrderBy(d => d.Location.SourceSpan.Start).ToArray();
78+
79+
var diag = analyzer.SupportedDiagnostics[0];
80+
var expected = new DiagnosticResult[] {
81+
diag.CreateDiagnosticResult(10, 31, "A"),
82+
diag.CreateDiagnosticResult(11, 31, "A"),
83+
diag.CreateDiagnosticResult(11, 33, "My"),
84+
diag.CreateDiagnosticResult(14, 33, "Awhile"),
85+
diag.CreateDiagnosticResult(15, 33, "And"),
86+
diag.CreateDiagnosticResult(18, 13, "A"),
87+
diag.CreateDiagnosticResult(18, 25, "A"),
88+
diag.CreateDiagnosticResult(25, 13, "IA"),
89+
diag.CreateDiagnosticResult(25, 23, "IA"),
90+
diag.CreateDiagnosticResult(32, 38, ".ctor"),
91+
diag.CreateDiagnosticResult(38, 38, ".ctor"),
92+
};
93+
94+
VerifyDiagnosticResults(diags, analyzer, expected);
95+
}
96+
}
97+
}

test/Microsoft.ML.CodeAnalyzer.Tests/Helpers/DiagnosticVerifier.cs

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ private void VerifyDiagnostics(string[] sources, DiagnosticAnalyzer analyzer, pa
8888
/// <param name="actualResults">The Diagnostics found by the compiler after running the analyzer on the source code</param>
8989
/// <param name="analyzer">The analyzer that was being run on the sources</param>
9090
/// <param name="expectedResults">Diagnostic Results that should have appeared in the code</param>
91-
private static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
91+
protected static void VerifyDiagnosticResults(IEnumerable<Diagnostic> actualResults, DiagnosticAnalyzer analyzer, params DiagnosticResult[] expectedResults)
9292
{
9393
int expectedCount = expectedResults.Length;
9494
int actualCount = actualResults.Count();
@@ -265,7 +265,7 @@ private static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diag
265265
private static readonly MetadataReference MLNetCoreReference = RefFromType<Runtime.IHostEnvironment>();
266266
private static readonly MetadataReference MLNetDataReference = RefFromType<Runtime.Model.ModelLoadContext>();
267267

268-
private static MetadataReference RefFromType<TType>()
268+
protected static MetadataReference RefFromType<TType>()
269269
=> MetadataReference.CreateFromFile(typeof(TType).Assembly.Location);
270270

271271
internal const string DefaultFilePathPrefix = "Test";
@@ -292,40 +292,22 @@ private static Diagnostic[] GetSortedDiagnostics(string[] sources, DiagnosticAna
292292
/// <param name="analyzer">The analyzer to run on the documents</param>
293293
/// <param name="documents">The Documents that the analyzer will be run on</param>
294294
/// <returns>An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location</returns>
295-
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents)
295+
protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, IEnumerable<Document> documents)
296296
{
297297
var projects = new HashSet<Project>();
298298

299-
foreach (var document in documents)
300-
{
301-
projects.Add(document.Project);
302-
}
299+
projects.UnionWith(documents.Select(d => d.Project));
303300

304301
var diagnostics = new List<Diagnostic>();
305302
foreach (var project in projects)
306303
{
307304
var comp = project.GetCompilationAsync().Result;
308305
var compilationWithAnalyzers = comp.WithAnalyzers(ImmutableArray.Create(analyzer));
309306
var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result;
310-
foreach (var diag in diags)
311-
{
312-
if (diag.Location == Location.None || diag.Location.IsInMetadata)
313-
{
314-
diagnostics.Add(diag);
315-
}
316-
else
317-
{
318-
for (int i = 0; i < documents.Length; i++)
319-
{
320-
var document = documents[i];
321-
var tree = document.GetSyntaxTreeAsync().Result;
322-
if (tree == diag.Location.SourceTree)
323-
{
324-
diagnostics.Add(diag);
325-
}
326-
}
327-
}
328-
}
307+
var projectTrees = new HashSet<SyntaxTree>(documents.Select(r => r.GetSyntaxTreeAsync().Result));
308+
309+
diagnostics.AddRange(diags.Where(d =>
310+
d.Location == Location.None || d.Location.IsInMetadata || projectTrees.Contains(d.Location.SourceTree)));
329311
}
330312

331313
var results = SortDiagnostics(diagnostics);
@@ -380,14 +362,27 @@ protected static Document CreateDocument(string source)
380362
/// <param name="sources">Classes in the form of strings</param>
381363
/// <returns>A Project created out of the Documents created from the source strings</returns>
382364
private static Project CreateProject(string[] sources)
365+
{
366+
Solution sol = null;
367+
return CreateProject(TestProjectName, ref sol, sources);
368+
}
369+
370+
/// <summary>
371+
/// Create a project using the input strings as sources.
372+
/// </summary>
373+
/// <param name="sources">Classes in the form of strings</param>
374+
/// <returns>A Project created out of the Documents created from the source strings</returns>
375+
internal static Project CreateProject(string projectName, ref Solution solution, params string[] sources)
383376
{
384377
string fileNamePrefix = DefaultFilePathPrefix;
385378

386-
ProjectId projectId = ProjectId.CreateNewId(debugName: TestProjectName);
379+
ProjectId projectId = ProjectId.CreateNewId(debugName: projectName);
380+
381+
if (solution == null)
382+
solution = new AdhocWorkspace().CurrentSolution;
387383

388-
var solution = new AdhocWorkspace()
389-
.CurrentSolution
390-
.AddProject(projectId, TestProjectName, TestProjectName, LanguageNames.CSharp)
384+
solution = solution
385+
.AddProject(projectId, projectName, projectName, LanguageNames.CSharp)
391386
.AddMetadataReference(projectId, CorlibReference)
392387
.AddMetadataReference(projectId, StandardReference)
393388
.AddMetadataReference(projectId, RuntimeReference)

test/Microsoft.ML.CodeAnalyzer.Tests/Helpers/TestUtils.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33
// See the LICENSE file in the project root for more information.
44

5+
using System;
56
using System.IO;
67
using System.Reflection;
78
using System.Threading;
@@ -26,13 +27,25 @@ public static ref string EnsureSourceLoaded(ref string source, string resourceNa
2627
{
2728
if (source == null)
2829
{
29-
string loadedSource;
30+
string loadedSource = LoadSource(resourceName);
3031
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
3132
using (var reader = new StreamReader(stream))
3233
loadedSource = reader.ReadToEnd();
3334
Interlocked.CompareExchange(ref source, loadedSource, null);
3435
}
3536
return ref source;
3637
}
38+
39+
public static string LoadSource(string resourceName)
40+
{
41+
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
42+
using (var reader = new StreamReader(stream))
43+
return reader.ReadToEnd();
44+
}
45+
46+
public static Lazy<string> LazySource(string resourceName)
47+
{
48+
return new Lazy<string>(() => LoadSource(resourceName), true);
49+
}
3750
}
3851
}

test/Microsoft.ML.CodeAnalyzer.Tests/Microsoft.ML.CodeAnalyzer.Tests.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
</EmbeddedResource>
88
</ItemGroup>
99

10+
<ItemGroup>
11+
<EmbeddedResource Include="..\..\src\Microsoft.ML.Core\BestFriendAttribute.cs" Link="Resources/BestFriendAttribute.cs">
12+
<LogicalName>%(Filename)%(Extension)</LogicalName>
13+
</EmbeddedResource>
14+
</ItemGroup>
15+
1016
<ItemGroup>
1117
<ProjectReference Include="..\..\tools-local\Microsoft.ML.InternalCodeAnalyzer\Microsoft.ML.InternalCodeAnalyzer.csproj" />
1218
<ProjectReference Include="..\..\src\Microsoft.ML.Analyzer\Microsoft.ML.Analyzer.csproj" />
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Runtime.CompilerServices;
2+
using Microsoft.ML;
3+
4+
[assembly: InternalsVisibleTo("ProjectB")]
5+
[assembly: WantsToBeBestFriends]
6+
7+
namespace Bubba
8+
{
9+
internal class A // Should fail.
10+
{
11+
public const int Hello = 2; // Fine by itself, but reference A.Hello will fail.
12+
internal static int My { get; } = 2; // Should also fail on its own merits.
13+
}
14+
15+
[BestFriend]
16+
internal class B // Should succeed.
17+
{
18+
[BestFriend]
19+
internal const string Friend = "Wave back when you wave hello."; // Should succeed.
20+
public const string Stay = "Don't hold their nose and point at you."; // Should succeed.
21+
internal const string Awhile = "Help you find your hat."; // Should Fail.
22+
23+
public B() { } // Should succeed.
24+
}
25+
26+
public class C : IA
27+
{
28+
internal const int And = 2; // Should Fail.
29+
[BestFriend]
30+
internal const int Listen = 2;// Should succeed.
31+
32+
[BestFriend]
33+
private protected C(int a) { } // Should succeed.
34+
internal C(float a) { } // Should Fail.
35+
}
36+
37+
public class D : IB
38+
{
39+
[BestFriend]
40+
internal D(int a) { } // Should succeed.
41+
private protected D(float a) { } // Should Fail.
42+
}
43+
44+
internal interface IA { } // Should Fail.
45+
[BestFriend]
46+
internal interface IB { } // Should succeed.
47+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using Bubba;
3+
4+
namespace McGee
5+
{
6+
class YoureMyBestFriend
7+
{
8+
public void Foo()
9+
{
10+
Console.WriteLine(A.Hello);
11+
Console.WriteLine(A.My);
12+
Console.WriteLine(B.Friend);
13+
Console.WriteLine(B.Stay);
14+
Console.WriteLine(B.Awhile);
15+
Console.WriteLine(C.And);
16+
Console.WriteLine(C.Listen);
17+
18+
var a = new A();
19+
var b = new B();
20+
var c = new C(2);
21+
c = new C(2.0f);
22+
var d = new D(2);
23+
d = new D(2.0f);
24+
25+
var da = (IA)c;
26+
var db = (IB)d;
27+
}
28+
29+
public class CDescend : C
30+
{
31+
public CDescend(int a) : base(a) { }
32+
public CDescend(float a) : base(a) { }
33+
}
34+
35+
public class DDescend : D
36+
{
37+
public DDescend(int a) : base(a) { }
38+
public DDescend(float a) : base(a) { }
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)