如何编写脚本从给定的 C# 源文件中删除多余的 using 指令?

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

似乎 Resharper 的 cleanupcode.exe 工具不知道这样做。

也许其他工具可以,但我没有找到。

尝试自己使用 Roslyn SemanticModel 到目前为止还没有成功,因为我不知道模型需要什么样的处理并且我找不到相关文档(或错过)。

所以,问题是 - 是否有工具可以识别实际使用的命名空间,或者甚至可以立即从给定的源文件中删除多余的 using 指令?也欢迎提出有关如何处理语义模型的想法。

编辑 1

考虑以下 C# 源代码:

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);
        }
    }
}

那么,我怎样才能确认using指令都是必需的呢?我尝试了以下代码:

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();
}

但它 returns 以下无意义的结果 - {"", "CSTool", "System", "Tests"}

根本不需要。

我被指出了以下问题 - 。但显然自 2012 年以来发生了很多变化,今天的答案没有任何意义。至少对我来说。

编辑 2

我是这样获取树和编译对象的:

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();
}

编辑 3

所以,我尝试了新方法。这是代码:

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);
    }
}

代码已经编译,所以我使用Mono.Cecil读取程序集引用列表,将它们转换为MetadataReferences并在创建CSharpCompilation对象时使用。

但是,diag 数组包含伪造的诊断消息,就像这个 - C:\xyz\tip\DB\DL\Db\FilterDB.cs(25,24): error CS0518: Predefined type 'System.String' 未定义或导入

这很假。手表 window 告诉我 compilation.References.First().Display 等于 C:\Program Files\dotnet\shared\Microsoft.NETCore.App.1.12\mscorlib.dll 正如我之前提到的 - 代码编译干净。

我做错了什么?

所以这个特定问题在 Roslyn 中是特殊情况——Roslyn 编译器会针对它知道未使用的语句发出诊断,然后您可以从那里处理这些语句。 GetSemanticModel.GetDiagnostics() 应该让您了解它们。 IDE 使用这些诊断来实现我们的“删除未使用的使用”功能。

为什么要用这种奇怪的方法?因为试图从其他 API 回答这个问题几乎是不可能的。 C# 中存在疯狂的边缘情况,您可以在其中删除 using 并且代码仍会编译, 但是 重载分辨率已更改,因此您的代码会更改含义。我们将它们计为“已使用”,但如果您的代码没有仔细了解重载决策如何考虑名称空间,您就不能这样做。

更一般地说,根据您尝试的目的,我们还有其他可能更有用的工具或 API。