Skip to content

Commit

Permalink
APIView - Visualize when assemblies expose internals via InternalsVis…
Browse files Browse the repository at this point in the history
…ibleTo (#7116)
  • Loading branch information
christothes authored Oct 26, 2023
1 parent d52c068 commit 21045c2
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 22 deletions.
66 changes: 60 additions & 6 deletions src/dotnet/APIView/APIView/Languages/CodeFileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.SymbolDisplay;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Linq;

namespace ApiView
Expand Down Expand Up @@ -50,7 +48,7 @@ public class CodeFileBuilder

public ICodeFileBuilderSymbolOrderProvider SymbolOrderProvider { get; set; } = new CodeFileBuilderSymbolOrderProvider();

public const string CurrentVersion = "23";
public const string CurrentVersion = "24";

private IEnumerable<INamespaceSymbol> EnumerateNamespaces(IAssemblySymbol assemblySymbol)
{
Expand Down Expand Up @@ -81,6 +79,7 @@ public CodeFile Build(IAssemblySymbol assemblySymbol, bool runAnalysis, List<Dep
var builder = new CodeFileTokensBuilder();

BuildDependencies(builder, dependencies);
BuildInternalsVisibleToAttributes(builder, assemblySymbol);

var navigationItems = new List<NavigationItem>();
foreach (var namespaceSymbol in SymbolOrderProvider.OrderNamespaces(EnumerateNamespaces(assemblySymbol)))
Expand Down Expand Up @@ -119,6 +118,37 @@ public CodeFile Build(IAssemblySymbol assemblySymbol, bool runAnalysis, List<Dep
return node;
}

public static void BuildInternalsVisibleToAttributes(CodeFileTokensBuilder builder, IAssemblySymbol assemblySymbol)
{
var assemblyAttributes = assemblySymbol.GetAttributes()
.Where(a =>
a.AttributeClass.Name == "InternalsVisibleToAttribute" &&
!a.ConstructorArguments[0].Value.ToString().Contains(".Tests") &&
!a.ConstructorArguments[0].Value.ToString().Contains(".Perf") &&
!a.ConstructorArguments[0].Value.ToString().Contains("DynamicProxyGenAssembly2"));
if (assemblyAttributes != null && assemblyAttributes.Any())
{
builder.Append("Exposes internals to:", CodeFileTokenKind.Text);
builder.NewLine();
foreach (AttributeData attribute in assemblyAttributes)
{
if (attribute.ConstructorArguments.Length > 0)
{
var param = attribute.ConstructorArguments[0].Value.ToString();
var firstComma = param.IndexOf(',');
param = firstComma > 0 ? param[..firstComma] : param;
builder.Append(new CodeFileToken(param, CodeFileTokenKind.Text)
{
// allow assembly to be commentable
DefinitionId = attribute.AttributeClass.Name
});
}
builder.NewLine();
}
builder.NewLine();
}
}

public static void BuildDependencies(CodeFileTokensBuilder builder, List<DependencyInfo> dependencies)
{
if (dependencies != null && dependencies.Any())
Expand Down Expand Up @@ -429,7 +459,7 @@ private void BuildAttributes(CodeFileTokensBuilder builder, ImmutableArray<Attri
const string attributeSuffix = "Attribute";
foreach (var attribute in attributes)
{
if (!IsAccessible(attribute.AttributeClass) || IsSkippedAttribute(attribute.AttributeClass))
if ((!IsAccessible(attribute.AttributeClass) && attribute.AttributeClass.Name != "FriendAttribute" )|| IsSkippedAttribute(attribute.AttributeClass))
{
continue;
}
Expand Down Expand Up @@ -505,6 +535,9 @@ private bool IsHiddenFromIntellisense(ISymbol member) =>
member.GetAttributes().Any(d => d.AttributeClass?.Name == "EditorBrowsableAttribute"
&& (EditorBrowsableState) d.ConstructorArguments[0].Value == EditorBrowsableState.Never);

private bool IsDecoratedWithAttribute(ISymbol member, string attributeName) =>
member.GetAttributes().Any(d => d.AttributeClass?.Name == attributeName);

private void BuildTypedConstant(CodeFileTokensBuilder builder, TypedConstant typedConstant)
{
if (typedConstant.IsNull)
Expand Down Expand Up @@ -584,9 +617,28 @@ private void DisplayName(CodeFileTokensBuilder builder, ISymbol symbol, ISymbol
builder.Keyword(SyntaxFacts.GetText(ToEffectiveAccessibility(symbol.DeclaredAccessibility)));
builder.Space();
}
foreach (var symbolDisplayPart in symbol.ToDisplayParts(_defaultDisplayFormat))
if (symbol is IPropertySymbol propSymbol && propSymbol.DeclaredAccessibility != Accessibility.Internal)
{
var parts = propSymbol.ToDisplayParts(_defaultDisplayFormat);
for (int i = 0; i < parts.Length; i++)
{
// Skip internal setters
if (parts[i].Kind == SymbolDisplayPartKind.Keyword && parts[i].ToString() == "internal")
{
while (i < parts.Length && parts[i].ToString() != "}")
{
i++;
}
}
builder.Append(MapToken(definedSymbol, parts[i]));
}
}
else
{
builder.Append(MapToken(definedSymbol, symbolDisplayPart));
foreach (var symbolDisplayPart in symbol.ToDisplayParts(_defaultDisplayFormat))
{
builder.Append(MapToken(definedSymbol, symbolDisplayPart));
}
}
}

Expand Down Expand Up @@ -692,6 +744,8 @@ private bool IsAccessible(ISymbol s)
case Accessibility.ProtectedOrInternal:
case Accessibility.Public:
return true;
case Accessibility.Internal:
return s.GetAttributes().Any(a => a.AttributeClass.Name == "FriendAttribute");
default:
return IsAccessibleExplicitInterfaceImplementation(s);
}
Expand Down
8 changes: 4 additions & 4 deletions src/dotnet/APIView/APIView/Languages/CompilationFactory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis;
Expand All @@ -11,7 +11,7 @@ namespace ApiView
{
public static class CompilationFactory
{
private static HashSet<string> AllowedAssemblies = new HashSet<string>(new []
private static HashSet<string> AllowedAssemblies = new HashSet<string>(new[]
{
"Microsoft.Bcl.AsyncInterfaces"
}, StringComparer.InvariantCultureIgnoreCase);
Expand Down Expand Up @@ -44,7 +44,7 @@ public static IAssemblySymbol GetCompilation(Stream stream, Stream documentation
// MetadataReference.CreateFromStream closes the stream
reference = MetadataReference.CreateFromStream(memoryStream, documentation: documentation);
}
var compilation = CSharpCompilation.Create(null).AddReferences(reference);
var compilation = CSharpCompilation.Create(null, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, metadataImportOptions: MetadataImportOptions.Internal)).AddReferences(reference);
var corlibLocation = typeof(object).Assembly.Location;

var runtimeFolder = Path.GetDirectoryName(corlibLocation);
Expand All @@ -63,4 +63,4 @@ public static IAssemblySymbol GetCompilation(Stream stream, Stream documentation
return (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol(reference);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

<ItemGroup>
<EmbeddedResource Include="ExactFormatting\*.cs" />
<EmbeddedResource Include="InternalsVisibleTo\*.cs" />
<ProjectReference Include="..\APIViewWeb\APIViewWeb.csproj" />
<ProjectReference Include="..\APIView\APIView.csproj" />
</ItemGroup>
Expand Down
35 changes: 26 additions & 9 deletions src/dotnet/APIView/APIViewUnitTests/CodeFileBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,41 @@ public CodeFileBuilderTests(ITestOutputHelper testOutputHelper)

private Regex _stripRegex = new Regex(@"/\*-\*/(.*?)/\*-\*/", RegexOptions.Singleline);

public static IEnumerable<object[]> ExactFormattingFiles
public static IEnumerable<object[]> FormattingFiles(string folder)
{
get
{
var assembly = typeof(CodeFileBuilderTests).Assembly;
return assembly.GetManifestResourceNames()
.Where(r => r.Contains("ExactFormatting"))
.Where(r => r.Contains(folder))
.Select(r => new object[] { r })
.ToArray();
}
}

[Theory]
[MemberData(nameof(ExactFormattingFiles))]
[MemberData(nameof(FormattingFiles), new object[] { "ExactFormatting" })]
public async Task VerifyFormatted(string name)
{
ExtractCodeAndFormat(name, out string code, out string formatted);
await AssertFormattingAsync(code, formatted);
}

[Theory]
[MemberData(nameof(FormattingFiles), new object[] { "InternalsVisibleTo" })]
public async Task VerifyFormattedWithInternalVisibleTo(string name)
{
ExtractCodeAndFormat(name, out string code, out string formatted);
formatted = $"Exposes internals to:{Environment.NewLine}Azure.Some.Client{Environment.NewLine}{Environment.NewLine}" + formatted;
await AssertFormattingAsync(code, formatted);
}

private void ExtractCodeAndFormat(string name, out string code, out string formatted)
{
var manifestResourceStream = typeof(CodeFileBuilderTests).Assembly.GetManifestResourceStream(name);
var streamReader = new StreamReader(manifestResourceStream);
var code = streamReader.ReadToEnd();
code = streamReader.ReadToEnd();
code = code.Trim(' ', '\t', '\r', '\n');
var formatted = _stripRegex.Replace(code, string.Empty);
formatted = _stripRegex.Replace(code, string.Empty);
formatted = RemoveEmptyLines(formatted);
formatted = formatted.Trim(' ', '\t', '\r', '\n');
await AssertFormattingAsync(code, formatted);
}

private async Task AssertFormattingAsync(string code, string formatted)
Expand All @@ -63,6 +74,12 @@ private async Task AssertFormattingAsync(string code, string formatted)
var formattedModel = new CodeFileRenderer().Render(codeModel).CodeLines;
var formattedString = string.Join(Environment.NewLine, formattedModel.Select(l => l.DisplayString));
_testOutputHelper.WriteLine(formattedString);
if(formatted != formattedString)
{
_testOutputHelper.WriteLine(String.Empty);
_testOutputHelper.WriteLine("Expected:");
_testOutputHelper.WriteLine(formatted);
}
Assert.Equal(formatted, formattedString);
}

Expand Down
2 changes: 1 addition & 1 deletion src/dotnet/APIView/APIViewUnitTests/Common.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class Common
public static async Task BuildDllAsync(Stream stream, string code)
{
var project = DiagnosticProject.Create(typeof(CodeFileBuilderTests).Assembly, LanguageVersion.Latest, new[] { code })
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
.WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, metadataImportOptions: MetadataImportOptions.Internal ));

var compilation = await project.GetCompilationAsync();
Assert.Empty(compilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Warning));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace A {
namespace A {
public class Class {
public Class(int a)/*-*/{/*-*/;/*-*/}/*-*/
protected Class()/*-*/{/*-*/;/*-*/}/*-*/
Expand All @@ -7,5 +7,6 @@ public class Class {
protected/*-*/ internal/*-*/ int C { get; set; }
/*-*/private protected int D { get; }/*-*/
/*-*/private int E { get; }/*-*/
public int F { get;/*-*/internal set;/*-*/ }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public abstract class Class2 {
}
public static class Class3 {
}
/*-*/internal class InternalClass { }/*-*/
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
namespace A {
public delegate int A(int b, bool d = false);
/*-*/internal delegate int B(int b, bool d = false);/*-*/
}
3 changes: 3 additions & 0 deletions src/dotnet/APIView/APIViewUnitTests/ExactFormatting/Enum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,7 @@ public enum NotFlags {
C = 2,
D = 7,
}
/*-*/internal enum InternalEnum {/*-*/
/*-*/A = 1/*-*/
/*-*/}/*-*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public class Class {
public const int I = 0;
public static readonly int R;
public string S;
/*-*/internal string T;/*-*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public class Class {
public void M3(string s, int m = 3)/*-*/{/*-*/;/*-*/}/*-*/
public Class M3(string s, int m = 3, DateTime d = default)/*-*/{ return null/*-*/;/*-*/}/*-*/
public void M4<T>()/*-*/{/*-*/;/*-*/}/*-*/
/*-*/internal void P() { }/*-*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public class Class {
public static int B { get; set; }
public int C { get; }
public int D { get; set; }
/*-*/internal int E { get; set; }/*-*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public readonly struct S1 {
public S1(int a)/*-*/{ A = a/*-*/;/*-*/}/*-*/
public int? A { get; }
}
}
/*-*/internal struct S2 { }/*-*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*-*/using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using C;

[assembly: InternalsVisibleTo("Azure.Some.Client")]
[assembly: InternalsVisibleTo("Azure.Some.Client.Tests")]
[assembly: InternalsVisibleTo("Azure.Some.Client.Perf")]
namespace C {
internal class FriendAttribute : Attribute {
public FriendAttribute(string friendAssemblyName) { }
}
}
internal interface IInternal
{
void M();
void N();
}/*-*/
[Friend("TestProject")]
internal interface IInternalWithFriend {
void M();
void N();
}
namespace A {
public interface I1 {
}
public interface I2<G> {
}
public abstract class K : I1 {
protected K()/*-*/{/*-*/;/*-*/}/*-*/
public abstract void M();
}
public abstract class L : IDisposable, IAsyncDisposable {
protected L()/*-*/{/*-*/;/*-*/}/*-*/
public abstract void Dispose();
public abstract ValueTask DisposeAsync();
}
[Friend("TestProject")]
internal abstract class M : IDisposable {
protected M()/*-*/{/*-*/;/*-*/}/*-*/
void IDisposable.Dispose()/*-*/{/*-*/;/*-*/}/*-*/
}
public class NClass : K, I1, I2<K> {
public NClass()/*-*/{/*-*/;/*-*/}/*-*/
public override sealed void M()/*-*/{/*-*/;/*-*/}/*-*/
}
public class OClass/*-*/ : IInternal/*-*/ {
public OClass()/*-*/{/*-*/;/*-*/}/*-*/
public void M()/*-*/{/*-*/;/*-*/}/*-*//*-*/
void IInternal.N(){}
/*-*/
}
public class PClass : IInternalWithFriend {
public PClass()/*-*/{/*-*/;/*-*/}/*-*/
void IInternalWithFriend.N()/*-*/{/*-*/;/*-*/}/*-*/
public void M()/*-*/{/*-*/;/*-*/}/*-*/
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*-*/
using System;
using System.Runtime.CompilerServices;
using B;

[assembly: InternalsVisibleTo("Azure.Some.Client")]
[assembly: InternalsVisibleTo("Azure.Some.Client.Tests")]
[assembly: InternalsVisibleTo("Azure.Some.Client.Perf")]
namespace B {
internal class FriendAttribute : Attribute {
public FriendAttribute(string friendAssemblyName) { }
}
}
/*-*/
namespace A {
public class PublicClass {
public PublicClass()/*-*/{/*-*/;/*-*/}/*-*/
/*-*/internal int InternalProperty { get; set; }/*-*/
[Friend("TestProject")]
internal void InternalMethodWithFriendAttribute()/*-*/{/*-*/;/*-*/}/*-*/
[Friend("TestProject")]
internal int InternalPropertyWithFriendAttribute { get; set; }
/*-*/internal void InternalMethod(){ }/*-*/
public void PublicMethod()/*-*/{/*-*/;/*-*/}/*-*/
public int PublicProperty { get; set; }
}
}

0 comments on commit 21045c2

Please sign in to comment.