How to Find All Types That Can Be Sealed Using Roslyn
In the previous post, I described how to use Roslyn as a library to find all types that could be internal
instead of public
. Let's continue this series by showing how to find all types that can be sealed
in a solution. Marking a type as sealed
can improve performance and safety by preventing further inheritance. Note that removing sealed
is not a breaking change, so you can safely mark types as sealed
without worrying about compatibility issues if you later decide to remove the sealed
modifier.
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 all symbols that can be sealed
. The code will iterate through all symbols in the solution, ask Roslyn to find derived classes to each symbol. If the symbol has no derived classes, it can be sealed
.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.MSBuild;
var solutionPath = @"Sample.sln";
Microsoft.Build.Locator.MSBuildLocator.RegisterDefaults();
var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(solutionPath);
foreach (var project in solution.Projects)
{
if (!project.SupportsCompilation)
continue;
var compilation = await project.GetCompilationAsync();
if (compilation is null)
continue;
foreach (var symbol in GetAllTypes(compilation.Assembly))
{
if (symbol is not INamedTypeSymbol namedTypeSymbol)
continue;
if (namedTypeSymbol.TypeKind is not TypeKind.Class)
continue;
if (namedTypeSymbol.IsSealed)
continue;
var derivedClasses = await SymbolFinder.FindDerivedClassesAsync(namedTypeSymbol, solution);
if (!derivedClasses.Any())
{
Console.WriteLine($"Symbol {symbol.ToDisplayString()} can be sealed");
}
}
}
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);
}
}
}
You can now run the code on your solution to find symbols that can be sealed
.
Do you have a question or a suggestion about this post? Contact me!