mark
mark

Reputation: 62886

How to script removal of redundant using directives from the given C# source file?

It does not seem Resharper's cleanupcode.exe tool knows to do it.

Maybe some other tool can, but I did not find any.

Trying to do it myself using Roslyn SemanticModel has not been successful so far, because I have no idea what kind of processing is needed on the model and I could not find relevant documentation (or missed it).

So, the question is - is there a tool to identify the actually used namespaces or maybe even remove the redundant using directives right away from the given source file? An idea on how to process the semantic model is welcome as well.

EDIT 1

Consider the following C# source code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace CSTool.Tests.Input
{
    public class Sample
    {
        public void Func()
        {
            static ICollection<ArrayList> action(string _) => null;

            Console.WriteLine(action(Directory.GetFiles(Directory.GetCurrentDirectory()).FirstOrDefault())?.Count);
        }
    }
}

So, how can I confirm that the using directives are all needed? I tried the following code:

private static IEnumerable<string> GetNamespaceSymbol(ISymbol symbol)
{
    if (symbol != null && symbol.ContainingNamespace != null)
    {
        yield return symbol.ContainingNamespace.Name;
    }
}

public static HashSet<string> Run(CSharpCompilation compilation, SyntaxTree tree)
{
    var semanticModel = compilation.GetSemanticModel(tree);
    return tree.GetRoot().DescendantNodes().SelectMany(node =>
        GetNamespaceSymbol(semanticModel.GetSymbolInfo(node).Symbol)).ToHashSet();
}

But it returns the following non-sensical result - {"", "CSTool", "System", "Tests"}

Not at all what is needed.

I was pointed at the following question - https://stackoverflow.com/a/9983275/2501279. But apparently a lot changed since 2012 and the answers do not make any sense today. At least to me.

EDIT 2

Here is how I obtained the tree and the compilation object:

public List<string> Test(string text, Type[] types)
{
    var tree = CSharpSyntaxTree.ParseText(text);
    var compilation = CSharpCompilation
        .Create("test")
        .AddReferences(types.Select(t => MetadataReference.CreateFromFile(t.Assembly.Location)))
        .AddSyntaxTrees(tree);
    return CleanupNamespacesCmd.Run(compilation, tree).OrderBy(ns => ns).ToList();
}

EDIT 3

So, I tried the new approach. Here is the code:

public static void Run(string projectFilePath)
{
    var project = new Project(projectFilePath);
    var outDir = project.GetPropertyValue("OutDir");
    using var a = AssemblyDefinition.ReadAssembly(project.GetPropertyValue("TargetPath"));
    var trees = MapTypesCmd
        .YieldCSFiles(project)
        .Select(filePath => CSharpSyntaxTree.ParseText(File.ReadAllText(filePath), path: filePath));
    var compilation = CSharpCompilation
        .Create(project.GetPropertyValue("AssemblyName"))
        .AddReferences(a.MainModule.AssemblyReferences.Select(CreateMetadataReference))
        .AddSyntaxTrees(trees);

    foreach (var tree in compilation.SyntaxTrees)
    {
        var model = compilation.GetSemanticModel(tree);
        var diag = model.GetDiagnostics();
    }

    MetadataReference CreateMetadataReference(AssemblyNameReference asmRef)
    {
        var asm = a.MainModule.AssemblyResolver.Resolve(asmRef);
        var path = asm.MainModule.FileName;
        if (!Path.IsPathRooted(path))
        {
            path = outDir + asm.MainModule.Name;
        }

        return MetadataReference.CreateFromFile(path);
    }
}

The code has already been compiled, so I use Mono.Cecil to read the list of assembly references, convert them to MetadataReferences and use when creating the CSharpCompilation object.

However, the diag array contains bogus diagnostic messages, like this one - C:\xyz\tip\DB\DL\Db\FilterDB.cs(25,24): error CS0518: Predefined type 'System.String' is not defined or imported

This is pretty bogus. The watch window tells me that compilation.References.First().Display equals C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.12\mscorlib.dll and as I have mentioned before - the code compiles cleanly.

What am I doing wrong?

Upvotes: 0

Views: 366

Answers (1)

Jason Malinowski
Jason Malinowski

Reputation: 19031

So this specific problem is special cased in Roslyn -- the Roslyn compiler emits diagnostics for using statements it knows are unused, and you can then process those from there. GetSemanticModel.GetDiagnostics() should let you get at them. The IDE uses these diagnostics to implement our "remove unused usings" feature.

Why this odd method? Because trying to answer this from other APIs is nearly impossible. There are crazy edge cases in C# where you can remove a using and the code will still compile, but overload resolution changed so your code changes meaning. We count those as "used" but without having your code know carefully how overload resolution considered the namespaces, you can't do that.

More generally, depending on what you're trying to do we have other tools or APIs that might be more useful.

Upvotes: 1

Related Questions