Search code examples
c#roslynsourcegenerators

Source generation: How to get involved types from InvocationExpressionSyntax


I'm try to write an source generator and need the types that involved in a extension method call.

The problem is, this extension method is generated by source generator itself. So if I try get ISymbol, I get null in my Generator class.

Is it possible to get the information I need otherwise?

Here is the example

var result = someObject.ConvertTo<OtherType>();

The ConvertTo<T>() extension method is generated from source generator. I can find the correct InvocationExpressionSyntax, but how can I get the fully qualified type of the someObject and OtherType?

Here is the generator

[Generator]
public class ConvertGenerator : ISourceGenerator
{
    private const string defaultNamespace = "AutoGenerators";
    private const string extensionsClassName = "ConvertExtensions";
    private static readonly string _classText = @$"
namespace {defaultNamespace}
{{
public static class {extensionsClassName}
{{
    public static TDestination ConvertTo<TDestination>(this object source)
    {{
        /* generated */

        return default;
    }}
}} }}";

    public void Initialize(GeneratorInitializationContext context)
    {
#if DEBUG
        if (!Debugger.IsAttached)
        {
            Debugger.Launch();
        }
#endif
        // Register a syntax receiver that will be created for each generation pass
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // retrieve the populated receiver 
        if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
            return;

        // for testing
        var invocationSyntax = receiver.Methods.FirstOrDefault();
        if (invocationSyntax != null)
        {
           var semanticModel = context.Compilation.GetSemanticModel(invocationSyntax.SyntaxTree);
           
           // symbol is null here
           var symbol = semanticModel.GetSymbolInfo(invocationSyntax.Expression).Symbol;
           
           // TODO: how to get type description for sourceObjectName and destinationTypeName
           var convertInvocationString = invocationSyntax.ToString();
           var sourceObjectName = convertInvocationString.Substring(0, convertInvocationString.IndexOf('.'));
           var destTypeSubs = convertInvocationString.Substring(convertInvocationString.IndexOf('<') + 1);
           var destinationTypeName = destTypeSubs.Substring(0, destTypeSubs.IndexOf('(') - 1);
           
        }

        var classSource = _classText;
        context.AddSource($"{extensionsClassName}.cs", SourceText.From(classSource, Encoding.UTF8));
    }

    /// <summary>
    /// Created on demand before each generation pass
    /// </summary>
    class SyntaxReceiver : ISyntaxReceiver
    {
        public List<InvocationExpressionSyntax> Methods { get; } = new List<InvocationExpressionSyntax>();

        /// <summary>
        /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation
        /// </summary>
        public void OnVisitSyntaxNode(SyntaxNode context)
        {
            // any field with at least one attribute is a candidate for property generation
            if (context is InvocationExpressionSyntax invocationExpressionSyntax
                && invocationExpressionSyntax.ToString().Contains("ConvertTo<"))
            {
                Methods.Add(invocationExpressionSyntax);
            }
        }
    }
}

Update: Also I think I need more then just the type. I need ISymbol to get all the properties of the types

Update 2: I did a small step by making the ConvertTo<T> method partial and reference the separat project with this method. I'm getting the IMethodSymbol now and have the ITypeSymbol for OtherType, but the ITypeSymbol for someObject is the object type, because of the extension method signature. But I need the concrete type symbol for someObject


Solution

  • I found the solution.

    First of all the ConvertTo<T> method should be declared in my project as partial, so I can get ISymbol for the invocation. It gives me the ReturnType

    var semanticModel = context.Compilation.GetSemanticModel(invocationSyntax.SyntaxTree);
    var mapToSymbol = semanticModel.GetSymbolInfo(invocationSyntax.Expression).Symbol as IMethodSymbol;
    var convertToType = mapToSymbol.ReturnType;
    

    Then I can use the invocationSyntax.Expression to get the type of the someObject or the parameter of the extension method

    var convertFromType = TryGetSourceType(invocationSyntax.Expression, semanticModel);
    
    ...
    
    private static ITypeSymbol TryGetSourceType(ExpressionSyntax invocationExpression, SemanticModel semanticModel)
    {
        switch (invocationExpression)
        {
            case MemberAccessExpressionSyntax memberAccessExpressionSyntax:
                var symbol = semanticModel.GetSymbolInfo(memberAccessExpressionSyntax.Expression).Symbol;
                return symbol switch
                {
                    ILocalSymbol local => local.Type,
                    IParameterSymbol param => param.Type,
                    IFieldSymbol field => field.Type,
                    IPropertySymbol prop => prop.Type,
                    IMethodSymbol method => method.MethodKind == MethodKind.Constructor ? method.ReceiverType : method.ReturnType,
                    _ => null
                };
            default:
                return null;
        }
    }