How to Find All Types That Can Be Sealed Using Roslyn

 
 
  • Gérald Barré

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:

csproj (MSBuild project 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.

Program.cs (C#)
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!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub