如何使用 Roslyn 检测脚本(而不是文档)中未使用的导入?

How can I detect unused imports in a Script (rather than a Document) with Roslyn?

提问人:Jon Skeet 提问时间:5/19/2017 最后编辑:Jon Skeet 更新时间:5/19/2017 访问量:2139

问:

我正在编写一个系统来处理作为 Noda Time 单元测试编写的片段,因此我可以将这些片段包含在文档中。我有一个第一遍工作,但我想整理代码。在处理代码段时,需要做的一件事是确定该代码段实际需要哪些指令。(单个源文件中可以有多个代码段,但每个代码段将单独显示在文档中 - 我不希望从一个代码段导入影响另一个代码段。using

工作代码处理实例 - 我创建一个单独的每个代码片段,其中包含一个方法和所有可能的导入,将其添加到项目中,然后删除不必要的指令,如下所示:DocumentDocumentusing

private async static Task<Document> RemoveUnusedImportsAsync(Document document)
{
    var compilation = await document.Project.GetCompilationAsync();
    var tree = await document.GetSyntaxTreeAsync();
    var root = tree.GetRoot();
    var unusedImportNodes = compilation.GetDiagnostics()
        .Where(d => d.Id == "CS8019")
        .Where(d => d.Location?.SourceTree == tree)
        .Select(d => root.FindNode(d.Location.SourceSpan))
        .ToList();
    return document.WithSyntaxRoot(
        root.RemoveNodes(unusedImportNodes, SyntaxRemoveOptions.KeepNoTrivia));
}

从那以后,我了解到在处理文档时可以使用 ,但我想把它写成一个 ,因为这在各个方面都感觉更干净。IOrganizeImportsServiceScript

创建脚本很容易,所以我只想分析未使用的导入(在一些早期的清理步骤之后)。以下是我希望适用于脚本的代码:

private static Script RemoveUnusedImports(Script script)
{
    var compilation = script.GetCompilation();
    var tree = compilation.SyntaxTrees.Single();
    var root = tree.GetRoot();
    var unusedImportNodes = compilation.GetDiagnostics()
        .Where(d => d.Id == "CS8019")
        .Where(d => d.Location?.SourceTree == tree)
        .Select(d => root.FindNode(d.Location.SourceSpan))
        .ToList();
    var newRoot = root.RemoveNodes(unusedImportNodes, SyntaxRemoveOptions.KeepNoTrivia);
    return CSharpScript.Create(newRoot.ToFullString(), script.Options);
}

不幸的是,这根本没有找到任何诊断 - 它们只是没有在编译:(中生成

下面是一个简短的示例应用,演示了这一点:

using System;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

class Program
{
    static void Main(string[] args)
    {
        string text = @"
using System;
using System.Collections.Generic;
Console.WriteLine(""I only need to use System"");";

        Script script = CSharpScript.Create(text);
        // Not sure whether this *should* be required, but it doesn't help...
        script.Compile();
        var compilation = script.GetCompilation();
        foreach (var d in compilation.GetDiagnostics())
        {
            Console.WriteLine($"{d.Id}: {d.GetMessage()}");
        }
    }
}

所需包:Microsoft.CodeAnalysis.CSharp.Scripting(例如 v2.1.0)

这不会产生输出:(

我的猜测是有意为之,因为脚本通常有不同的用例。但是,有没有办法为脚本目的启用更多诊断?或者有没有其他方法可以检测中未使用的导入?如果没有,我将回到我基于的方法 - 这将是一个遗憾,因为其他所有内容似乎都与脚本配合得很好......ScriptDocument

C# Roslyn 使用指令

评论


答:

13赞 daveaglick 5/19/2017 #1

据我所知,脚本引擎中的默认编译不会为语法错误配置任何诊断。遗憾的是,脚本引擎只有有限的选项来自行配置底层编译。

但是,您可以通过跳过脚本引擎并直接自己创建编译来实现您想要的目标。这基本上是脚本主机在幕后所做的,它添加了一些编译的默认值以及一些花哨的东西,例如提升类声明。跳过脚本主机并自行创建编译的代码如下所示:

using System;
using System.IO;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

class Program
{
    static void Main(string[] args)
    {
        string text = @"
using System;
using System.Collections.Generic;
Console.WriteLine(""I only need to use System"");";

        SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(text, new CSharpParseOptions(kind: SourceCodeKind.Script));
        var coreDir = Path.GetDirectoryName(typeof(object).GetTypeInfo().Assembly.Location);
        var mscorlib = MetadataReference.CreateFromFile(Path.Combine(coreDir, "mscorlib.dll"));
        var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
        var compilation = CSharpCompilation.Create("MyAssembly")
            .AddSyntaxTrees(syntaxTree)
            .AddReferences(mscorlib)
            .WithOptions(options);
        foreach (var d in compilation.GetDiagnostics())
        {
            Console.WriteLine($"{d.Id}: {d.GetMessage()}");
        }
    }
}

您会注意到这会产生一些关于缺少引用等的不良诊断 - 编译引用需要稍微调整以包含默认库(您可以在上面看到带有 mscorlib 的模式)。您还应该看到有关未使用的 using 语句的所需诊断。

评论

0赞 Jon Skeet 5/19/2017
谢谢 - 经过一些调整,这似乎有效。奇怪的是,似乎将 SyntaxTree 选项的种类从“脚本”更改为“常规”,所以我必须在替换后再次调整它,但似乎没问题......SyntaxNode.ReplaceNodes
1赞 Filip W 5/19/2017
请注意,我不久前在 Roslyn 上为这件事打开了一个问题 github.com/dotnet/roslyn/issues/19329
0赞 svick 5/22/2017
@JonSkeet 你到底是如何更新树的?据我所知,你不能直接在 上使用 ,因为它不是 .ReplaceNodesSyntaxTreeSyntaxNode
0赞 svick 5/22/2017
@JonSkeet 好吧,让我感到困惑的是你说的,但你的代码使用了.无论如何,我仍然没有看到任何代码可以指示您如何创建新的,但是我尝试的所有合理方法都对我有用。也就是说,除非您忘记传递选项,例如 .请参阅此要点ReplaceNodesRemoveNodesSyntaxTreeSyntaxFactory.SyntaxTree(newRoot)
0赞 Jon Skeet 5/22/2017
@svick: I&#39;从未将 SyntaxFactory.SyntaxTree 称为 I&#39;我从来不需要&#39;我知道。但是,在此之后不久,请参阅我关于选项的另一个问题 - 看起来丢失的选项只是一个错误。(新的语法树是通过调用 RemoveNodes 或 ReplaceNodes 隐式创建的。毕竟,返回的语法根必须在树中......