20

I'm writing a system to process snippets written as unit tests for Noda Time, so I can include the snippets in the documentation. I've got a first pass working, but I wanted to tidy up the code. One of the things this needs to do when processing a snippet is work out which of the using directives are actually required for that snippet. (There can be multiple snippets in a single source file, but each snippet will appear separately in the documentation - I don't want imports from one snippet affecting another.)

The working code deals with Document instances - I create a separate Document per snippet containing a single method and all the potential imports, add it to the project, and then remove unnecessary using directives like this:

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

I've since learned that I could use the IOrganizeImportsService when working with a document, but I'd like to just write it as a Script, as that feels much cleaner in various ways.

Creating the script is easy, so I'd like to just analyze that for unused imports (after some earlier cleanup steps). Here's code I'd hoped would work for a script:

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

Unfortunately, that doesn't find any diagnostics at all - they're just not produced in the compilation :(

Here's a short sample app demonstrating that:

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

Required package: Microsoft.CodeAnalysis.CSharp.Scripting (e.g. v2.1.0)

This produces no output :(

My guess is that this is intended, because scripting usually has different use cases. But is there any way of enabling more diagnostics for scripting purposes? Or is there some alternative way of detecting unused imports in a Script? If not, I'll go back to my Document-based approach - which would be a pity, as everything else seems to work quite nicely with scripts...

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194

1 Answers1

13

As far as I know, the default compilation in the scripting engine doesn't configure diagnostics for anything but syntax errors. Unfortunately the scripting engine only has limited options to configure the underlying compilation yourself.

However, you can probably achieve what you're after by skipping the scripting engine and directly creating the compilation yourself. This is essentially what the script host does behind the scenes with the addition of some of the defaults for the compilation as well as a few fancy things like lifting class declarations. The code to skip the script host and create the compilation yourself would look something like:

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

You'll notice this produces some undesirable diagnostics about missing references and such - the compilation references need to be tweaked a little to include the default libraries (you can see the pattern with mscorlib above). You should see the desired diagnostics about unused using statements as well.

daveaglick
  • 3,600
  • 31
  • 45
  • Thanks - with a bit of tweaking, that appears to work. Oddly enough, `SyntaxNode.ReplaceNodes` appears to change the kind of the SyntaxTree options from "Script" to "Regular", so I have to tweak it back again after my replacements, but then it seems okay... – Jon Skeet May 19 '17 at 01:13
  • 1
    note that I opened an issue on Roslyn for this very thing a while ago https://github.com/dotnet/roslyn/issues/19329 – Filip W May 19 '17 at 04:33
  • @JonSkeet How exactly are you updating the tree? As far as I can tell, you can't use `ReplaceNodes` directly on a `SyntaxTree`, because it's not a `SyntaxNode`. – svick May 21 '17 at 20:40
  • @JonSkeet Ok, what confused me is that you talk about `ReplaceNodes`, but your code uses `RemoveNodes`. Anyway, I still don't see any code that would indicate how you create the new `SyntaxTree`, but all the reasonable approaches I tried work for me. That is, except if you forgot to pass the options, e.g. `SyntaxFactory.SyntaxTree(newRoot)`. See [this gist](https://gist.github.com/svick/e52d523457b7a27fe6fd6b19a0842c36). – svick May 21 '17 at 22:33
  • @svick: I&#39;ve never called SyntaxFactory.SyntaxTree as I&#39;ve never needed to as far as I&#39;m aware. But see my other question shortly after this one around the options - it looks like the options being lost is just a bug. (The new syntax tree is implicitly created by calling RemoveNodes or ReplaceNodes. After all, the returned syntax root has to be in a tree...) – Jon Skeet May 21 '17 at 22:36