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

APIView - Visualize when assemblies expose internals via InternalsVisibleTo #7116

Merged
merged 8 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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; }
}
}