How to Find Public Symbols That Can Be Internal Using Roslyn
I got a question about how to find all public symbols in a solution that can be internal. This is a common scenario when you want to reduce the visibility of your code to improve encapsulation. You can use Roslyn as a library to analyze the code in a solution and find such symbols.
I've already written about how to use Roslyn as a library to analyze and rewrite code in a solution: Using Roslyn to analyze and rewrite code in a solution. This post will build on that knowledge and show you how to find public symbols that can be internal.
First, let's create a console application and add the necessary NuGet packages. You can use the following .csproj
file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Locator" Version="1.9.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.14.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
</ItemGroup>
</Project>
Next, you can use the following code to analyze the solution and find public symbols that can be internal. The code will iterate through all symbols in the solution, ask Roslyn to find references to each symbol, and check if the symbol is referenced outside the project. If it is not referenced outside the project, it can be internal.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.MSBuild;
var solutionPath = @"Sample.sln";
// Open the solution using MSBuild
Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults();
var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(solutionPath);
// Iterate through all projects in the solution
foreach (var project in solution.Projects)
{
if (!project.SupportsCompilation)
continue;
// Get the compilation for the project
var compilation = await project.GetCompilationAsync();
if (compilation is null)
continue;
// For each public symbol in the compilation, check its usages.
// If it is not referenced outside the project, it can be internal.
// Note that this is a simplified example and may not cover all cases.
// Some types may be public because some libraries require them to be public.
foreach (var symbol in GetAllTypes(compilation.Assembly))
{
// Compute the visibility of the symbol. For instance, a public class can be private
// if it is declared inside a private class (nested).
var visibility = GetResultantVisibility(symbol);
if (visibility is not SymbolVisibility.Public)
continue;
var canBeInternal = true;
// Check if the symbol is referenced outside the project
var references = await SymbolFinder.FindReferencesAsync(symbol, solution);
foreach (var reference in references)
{
foreach (var location in reference.Locations)
{
if (location.Document.Project != project)
{
canBeInternal = false;
}
}
}
// Report the symbol if it can be internal
// Note that you can go further and rewrite the code to make it internal (see the previous post).
if (canBeInternal)
{
Console.WriteLine($"Public symbol {symbol.ToDisplayString()} can be internal");
}
}
}
static IEnumerable<ITypeSymbol> GetAllTypes(IAssemblySymbol assembly)
{
var result = new List<ITypeSymbol>();
foreach (var module in assembly.Modules)
{
ProcessNamespace(result, module.GlobalNamespace);
}
return result;
static void ProcessNamespace(List<ITypeSymbol> result, INamespaceSymbol ns)
{
foreach (var type in ns.GetTypeMembers())
{
ProcessType(result, type);
}
foreach (var nestedNs in ns.GetNamespaceMembers())
{
ProcessNamespace(result, nestedNs);
}
}
static void ProcessType(List<ITypeSymbol> result, ITypeSymbol symbol)
{
result.Add(symbol);
foreach (var type in symbol.GetTypeMembers())
{
ProcessType(result, type);
}
}
}
// Copied from https://github.com/dotnet/roslyn/blob/d2ff1d83e8fde6165531ad83f0e5b1ae95908289/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/ISymbolExtensions.cs#L28-L73
static SymbolVisibility GetResultantVisibility(ISymbol symbol)
{
// Start by assuming it's visible.
var visibility = SymbolVisibility.Public;
switch (symbol.Kind)
{
case SymbolKind.Alias:
// Aliases are uber private. They're only visible in the same file that they
// were declared in.
return SymbolVisibility.Private;
case SymbolKind.Parameter:
// Parameters are only as visible as their containing symbol
return GetResultantVisibility(symbol.ContainingSymbol);
case SymbolKind.TypeParameter:
// Type Parameters are private.
return SymbolVisibility.Private;
}
while (symbol is not null && symbol.Kind != SymbolKind.Namespace)
{
switch (symbol.DeclaredAccessibility)
{
// If we see anything private, then the symbol is private.
case Accessibility.NotApplicable:
case Accessibility.Private:
return SymbolVisibility.Private;
// If we see anything internal, then knock it down from public to
// internal.
case Accessibility.Internal:
case Accessibility.ProtectedAndInternal:
visibility = SymbolVisibility.Internal;
break;
// For anything else (Public, Protected, ProtectedOrInternal), the
// symbol stays at the level we've gotten so far.
}
symbol = symbol.ContainingSymbol;
}
return visibility;
}
enum SymbolVisibility
{
Public,
Internal,
Private,
}
You can now run the code on your solution to find public symbols that can be internal. You can even create a unit test to verify it automatically.
Do you have a question or a suggestion about this post? Contact me!