CodeChops
CodeChops

Reputation: 222

C# source generators: Is there a way to find type information of referenced projects?

Using C# source generators, is there a way to get more information about types in referenced assemblies. To be more precise: Is there a way to find out which type implements an interface that resides in a referenced project?

For example:

Assembly 1 -BaseClass with interface

Assembly 2 (uses the source generator and refers to assembly 1) -Implements BaseClass of Assembly1

Thanks in advance.

Upvotes: 5

Views: 3003

Answers (1)

Dima
Dima

Reputation: 360

Yes, there is a way. I've done that for one of my source generator projects. Because I don't assume which adjustments you'd need, I'll just drop the essential code here and then highlight some of the things which might be relevant to you:

internal interface IImplementationTypeSetCache
{
    IImmutableSet<INamedTypeSymbol> All { get; }

    IImmutableSet<INamedTypeSymbol> ForAssembly(IAssemblySymbol assembly);
}

internal class ImplementationTypeSetCache : IImplementationTypeSetCache
{
    private readonly GeneratorExecutionContext _context;
    private readonly WellKnownTypes _wellKnownTypes;
    private readonly Lazy<IImmutableSet<INamedTypeSymbol>> _all;
    private IImmutableDictionary<IAssemblySymbol, IImmutableSet<INamedTypeSymbol>> _assemblyCache =
        ImmutableDictionary<IAssemblySymbol, IImmutableSet<INamedTypeSymbol>>.Empty;

    private readonly string _currentAssemblyName;

    internal ImplementationTypeSetCache(
        GeneratorExecutionContext context,
        WellKnownTypes wellKnownTypes)
    {
        _context = context;
        _wellKnownTypes = wellKnownTypes;
        _currentAssemblyName = context.Compilation.AssemblyName ?? "";
        _all = new Lazy<IImmutableSet<INamedTypeSymbol>>(
            () => context
                .Compilation
                .SourceModule
                .ReferencedAssemblySymbols
                .Prepend(_context.Compilation.Assembly)
                .SelectMany(ForAssembly)
                .ToImmutableHashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default));
    }

    public IImmutableSet<INamedTypeSymbol> All => _all.Value;
    public IImmutableSet<INamedTypeSymbol> ForAssembly(IAssemblySymbol assembly)
    {
        if (_assemblyCache.TryGetValue(assembly, out var set)) return set;

        var freshSet = GetImplementationsFrom(assembly);
        _assemblyCache = _assemblyCache.Add(assembly, freshSet);
        return freshSet;
    }

    private IImmutableSet<INamedTypeSymbol> GetImplementationsFrom(IAssemblySymbol assemblySymbol)
    {
        var internalsAreVisible = 
            SymbolEqualityComparer.Default.Equals(_context.Compilation.Assembly, assemblySymbol) 
            ||assemblySymbol
                .GetAttributes()
                .Any(ad =>
                    SymbolEqualityComparer.Default.Equals(ad.AttributeClass, _wellKnownTypes.InternalsVisibleToAttribute)
                    && ad.ConstructorArguments.Length == 1
                    && ad.ConstructorArguments[0].Value is string assemblyName
                    && Equals(assemblyName, _currentAssemblyName));
                
        return GetAllNamespaces(assemblySymbol.GlobalNamespace)
            .SelectMany(ns => ns.GetTypeMembers())
            .SelectMany(t => t.AllNestedTypesAndSelf())
            .Where(nts => nts is
            {
                IsAbstract: false,
                IsStatic: false,
                IsImplicitClass: false,
                IsScriptClass: false,
                TypeKind: TypeKind.Class or TypeKind.Struct or TypeKind.Structure,
                DeclaredAccessibility: Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal
            })
            .Where(nts => 
                !nts.Name.StartsWith("<") 
                && (nts.IsAccessiblePublicly() 
                    || internalsAreVisible && nts.IsAccessibleInternally()))
            .ToImmutableHashSet<INamedTypeSymbol>(SymbolEqualityComparer.Default);
    }

    private static IEnumerable<INamespaceSymbol> GetAllNamespaces(INamespaceSymbol root)
    {
        yield return root;
        foreach(var child in root.GetNamespaceMembers())
        foreach(var next in GetAllNamespaces(child))
            yield return next;
    }
}

_wellKnownTypes.InternalsVisibleToAttribute is just the INamedTypeSymbol instance representing the .Net InternalsVisibleToAttribute. Further more an extension method is in use here:

internal static IEnumerable<INamedTypeSymbol> AllNestedTypesAndSelf(this INamedTypeSymbol type)
    {
        yield return type;
        foreach (var typeMember in type.GetTypeMembers())
        {
            foreach (var nestedType in typeMember.AllNestedTypesAndSelf())
            {
                yield return nestedType;
            }
        }
    }

I use this code to iterate over all implementation types (at least what my project considers as such) from the current assembly and all (!) referenced assemblies (my assemblies, third-party assemblies and .Net assemblies). So you might consider making some adjustments and therefore I want to highlight some points.

First, you'll get referenced assemblies by:

context
    .Compilation
    .SourceModule
    .ReferencedAssemblySymbols

Like mentioned before these are really all referenced assemblies. So, you might want to filter them in order to prevent redundancies.

Next, consider accessibility. Does your target assembly disclose the internals to your source generator project via InternalsVisibleToAttribute?

Last, you might need to adjust the filter logic of the types, because it is specific to my project. For example, you might want to include abstract classes as well.

The rest of the code is basically the logic for iterating over assemblies down to namespaces down to types down to nested types.

Finally, you'll just need to check which of the iterated types implement the interface.

That should be it. Have fun.

Now that I consider my work done (but feel free to ask follow-up questions), I hope that I deserve a mini-advertisement:

This snippets that I pasted here are part of my dependency injection container project MrMeeseeks.DIE (documentation). Feedback is very welcome.

Upvotes: 5

Related Questions