Search code examples
c#autocompleteroslynroslyn-code-analysis

Roslyn CompletionService does not infer type for implicit foreach variable


I'm using Roslyn's CompletionService for autocomplete functionality in my app, that allows users to write and execute c# scripts.

When writing an expression such as (pipe denotes the caret position):

var m = Selected.Measure;
m.|

Roslyn is perfectly able to infer that m is a variable of the same type as the Measure property, and it suggests all of its properties, methods, etc. for autocomplete, as expected.

However, in the following expression, where Measures is an IEnumerable<Measure>:

foreach(var m in Selected.Measures)
{
    m.|
}

...the Roslyn CompletionService does not provide any suggestions. Specifically, this code returns 0 CompletionItems:

sourceText = SourceText.From(text);
document = document.WithText(sourceText);
var completionService = CompletionService.GetService(document);
return await completionService.GetCompletionsAsync(document, position).ConfigureAwait(false);

Am I missing some configuration of the CompletionService or the MefHostService in order to allow type inference of implicit foreach variables (which is something that Visual Studio seems to do just fine)?

I'm using the following code to configure Roslyn:

var assemblies = new List<Assembly>(MefHostServices.DefaultAssemblies);
assemblies.Add(typeof(ScriptEngine).Assembly); // This is the assembly that contains the Selected class, the Measure class, etc.
var host = MefHostServices.Create(assemblies);
this.workspace = new AdhocWorkspace(host);

var compilationOptions = new CSharpCompilationOptions(
    OutputKind.DynamicallyLinkedLibrary,
    usings: new[] { "System" })
    .WithUsings(ScriptEngine.DefaultUsings);
var scriptProjectInfo = ProjectInfo.Create(ProjectId.CreateNewId(), 
    VersionStamp.Create(), "Script", "Script",
    LanguageNames.CSharp, isSubmission: true, hostObjectType: typeof(ScriptHost))
    .WithMetadataReferences(new[] { 
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(ScriptEngine).Assembly.Location
    })
    .WithParseOptions(new CSharpParseOptions(LanguageVersion.Latest, kind: SourceCodeKind.Script))
    .WithCompilationOptions(compilationOptions);

sourceText = SourceText.From("");
var scriptProject = workspace.AddProject(scriptProjectInfo);
var scriptDocumentInfo = DocumentInfo.Create(
    DocumentId.CreateNewId(scriptProject.Id), "Script",
    sourceCodeKind: SourceCodeKind.Script,
    loader: TextLoader.From(TextAndVersion.Create(sourceText, VersionStamp.Create())));
document = workspace.AddDocument(scriptDocumentInfo);

Solution

  • When providing MetadataReferences to Roslyn, we must explicitly provide transient dependencies. It is not enough to pass a reference to the typeof(object).Assembly.

    Replacing this code:

    .WithMetadataReferences(new[] { 
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(ScriptEngine).Assembly.Location
    })
    

    with:

    var metadataReferences = ((String)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")).Split(Path.PathSeparator)
        .Select(l => MetadataReference.CreateFromFile(l)).ToList();
    metadataReferences.Add(MetadataReference.CreateFromFile(typeof(ScriptEngine).Assembly.Location));
    // ...
    .WithMetadataReferences(metadataReferences)
    

    did the trick!

    See also: