How Do I Transplant and Execute User Code in a C# Source Generator?

100 Views Asked by At

I am creating a Roslyn .NET source generator. With it I am inspecting user code for lambda expressions given as an argument in a method call like this:

// User Code
MyLibrary.MyMethod<Guid>(k => $"alfa/{k.SomeMethod()}");

I want to, within my source generator at compile-time, take the LambdaExpressionSyntax node k => $"alfa/{k.SomeMethod()}" and transplant it to some simple code I will generate and run to interrogate it for e.g. its run-time expression tree shape or call its .ToString() method:

// Generated Code
using System;
using Etc;
Expression<Func<Guid, string>> expression = k => $"alfa/{k.SomeMethod()}";
return expression;

I have so far discovered the CSharpScript.EvaluateAsync() method, which seems like the easiest way to compile and execute the code.

Unfortunately, this (and probably any) approach requires me to sus out what the dependencies are of the expression I am trying to transplant, so that I can generate the appropriate using statements and add the required library references to the compilation.

For example, SomeMethod() above is an extension method that lives in a separate class library from the inspected user code. Its namespace and class library will need to be included in the usings and compilation references.

Supposing I have a magic method to get the referenced types, what I have so far is something like this:

var referencedTypes = GetReferencedTypes(lambdaExpressionSyntax);

var result = CSharpScript
    .EvaluateAsync(
        code,
        ScriptOptions.Default
            .AddReferences(
                typeof(Func<,>).Assembly,
                typeof(Expression).Assembly)
            .AddReferences(referencedTypes
                .Select(t => t.ContainingAssembly.GetMetadata().GetReference())
                .ToArray()))
    .Result;

Unfortunately the way I am trying to add the references does not appear to work. I get an error at runtime that the assembly could not be found.

So my questions are:

  1. How do I correctly determine all of the dependencies for a given LambdaExpressionSyntax?
  2. How do I properly add references to those dependencies when calling CSharpScript.EvaluateAsync()?
  3. Is there a better or simpler way of achieving what I am trying to do?
1

There are 1 best solutions below

1
On BEST ANSWER

I was able to achieve the above pretty simply once I realized that the using directives and references needed for the transplanted code must be a subset of the user code I am transplanting from. So the relevant section of my source generator's Execute method has become:

var code = $$"""
    Expression<Func<{{keyType.Name}}, string>> expr = {{lambdaExpression}};
    return expr;
    """;

var imports = lambdaExpression.SyntaxTree
    .GetRoot()
    .DescendantNodes()
    .OfType<UsingDirectiveSyntax>()
    .Select(u => u.Name.ToString())
    .Append("System")
    .Append("System.Linq.Expressions")
    .Append(keyType.ContainingNamespace.ToString())
    .Distinct()
    .OrderBy(u => u)
    .ToList();

var options = ScriptOptions.Default
    .AddReferences(
        typeof(Func<,>).Assembly,
        typeof(Expression).Assembly)
    .AddReferences(
        compilation.References)
    .AddImports(imports);

var result = await CSharpScript.EvaluateAsync(code, options);

Critically, I give the AddReferences and AddImports methods the values derived from the user code's syntax tree and compilation.

This still will not work if the transplanted user code references e.g. a local variable or class member. These are not supported use cases for my efforts, so I will likely just add graceful error handling for these occasions.