Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Reading attribute properties from inside a source generator

BACKGROUND

I'm trying my hand at C# source generators. This is primarily an experimental project, but the goal is to create a library that will assist the user in writing logs using a key-value pair format. There is meant to be a pre-defined set of common keys at compile time. The library won't know what these are until the user declares an enum and decorates it with a special attribute. The enum members can optionally be decorated with an additional attribute that declares what Type the key is (if the attribute is absent, we assume it is a string). Here are the enums I am using:

[AttributeUsage(AttributeTargets.Enum)]
public class LogKeysAttribute : Attribute { }

[System.AttributeUsage(System.AttributeTargets.Field)]
public class LogMemberAttribute : Attribute
{
    public Type FieldType { get; set; }
}

So, as an example, we might have something like this:

[LogKeys]
public enum CommonKeys
{
    [LogMember(FieldType = typeof(int))]    FirstAsInt     = 1,
    [LogMember(FieldType = typeof(string))] SecondAsString = 2,
    ThirdAsImpliedString = 5,
}

Now what the library is going to do is create a new method like this:

public void Log(int? firstAsInt=null, string secondAsString=null, string thirdAsImpliedString = null)
{
    ...
}

PROBLEM

So far, I've gotten just about everything working the way I want, but I'm hung up on the part where I read the value of FieldType in the LogMemberAttribute. I can see if the LogMember is there, and in the debugger, I can see that it was set to an int or string, but in the code I can't figure out how to pull that out and make use of it.

SOURCE CODE Here is (a simplified version of) my Execute method (NOTE: I realize there are things that are sub-optimal here. That's OK--right now we are only worried about reading from attributes):

public void Execute(GeneratorExecutionContext context)
{
    var compilation      = context.Compilation;

    //Declare our attributes that will be used
    //I had hoped adding it here would make the type available to use below, but no such luck
    var attributeDefinitionsSrc = @"
using System;
namespace LogWrapper
{
  [AttributeUsage(AttributeTargets.Enum)]
  public class LogKeysAttribute : Attribute { }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class LogMemberAttribute : Attribute
  {
    public Type FieldType { get; set; }
  }
}";

    //Add the attributes to the compilation
    context.AddSource("CustomAttributes.cs", attributeDefinitionsSrc);
    var attribSyntaxTree = CSharpSyntaxTree.ParseText(attributeDefinitionsSrc, (CSharpParseOptions)context.ParseOptions);
    compilation = compilation.AddSyntaxTrees(attribSyntaxTree);

    //Now we can get those symbols to use later
    var keyAttribute  = compilation.GetTypeByMetadataName("LogWrapper.LogKeyAttribute");
    var memberTypeAttr = compilation.GetTypeByMetadataName("LogWrapper.LogMemberAttribute");

    //targets will be anything that is an Enum declaration
    var targets = compilation.SyntaxTrees
                                 .SelectMany(x => x.GetRoot()
                                                   .DescendantNodesAndSelf()
                                                   .OfType<EnumDeclarationSyntax>());
    foreach (var t in targets)
    {
//bonus points if you can explain to me what the "SemanticModel" is and how it differs from a syntax tree.
//I see you can view the syntax tree at sharplab.io. Is there a place/way to view the semantic model?
        var targetType = ModelExtensions
                           .GetDeclaredSymbol(compilation.GetSemanticModel(t.SyntaxTree), t);

        //We are only interested in the first enum that has our KeyAttribute associated
        if (targetType != null &&
            targetType.GetAttributes().Any(x => x.AttributeClass.Equals(keyAttribute)) &&
            targetType is ITypeSymbol its)
        {
            //Here we get all the enum members (FirstAsInt, SecondAsString, etc.)
            //They are sorted by their enum value (FirstAsInt is 1, so it goes first)
            var iSymbolList = its.GetMembers()
                              .Where(x=>x is IFieldSymbol ifs && ifs.HasConstantValue)
                              .OrderBy(x => ((IFieldSymbol)x).ConstantValue)
                              .ToList();

            //Next we iterate over all the members
            foreach (var sym in iSymbolList)
            {
                //If this member has a LogMember attribute associated with it,
                // then we get that attribute.
                var attrib = sym.GetAttributes()
                          .FirstOrDefault(x=>x.AttributeClass?.Equals(memberTypeAttr) == true);
                if (attrib != default)
                {
                    //There was a LogMember attribute. This means I want to read the
                    // value of FieldType so I know what Type it should be.

                    // **** THIS IS THE PART THAT I CAN'T DO *****

   //Debugger shows list of 1 with value "{[FieldType, {Microsoft.CodeAnalysis.TypedConstant}]}"
                    var namedArgsList = attrib.NamedArguments;

   //Debugger shows an empty list
                    var constArgsList = attrib.ConstructorArguments;

   //Debugger shows "{[FieldType, {Microsoft.CodeAnalysis.TypedConstant}]}"
                    var firstArg      = namedArgsList.FirstOrDefault(x => x.Key == "FieldType");

   //Debugger shows value "{int}" and it shows the type as:
   //"object { Microsoft.CodeAnalysis.CSharp.Symbols.PublicModel.NonErrorNamedTypeSymbol}"
   //It seems the data I want is here, but I can't get to it...
                    var valAsObj      = firstArg.Value.Value;

   //Debugger shows null. We can't cast to a Type.
                    var asType        = valAsObj as Type;
                    }
                    //else assume string (code removed)
                }

                //by this point we have all the data we need to generate the code
                break;
            }
        }

So is it possible to fetch this data? I'm afraid I'm not very good with SyntaxTrees and whatnot, so it seems likely there's something I could be doing that would give me what I want, but I just don't know what that is.

like image 808
David Avatar asked May 23 '26 05:05

David


1 Answers

Many questions here, but:

//Debugger shows null. We can't cast to a Type.

The value in this case should be castable to INamedTypeSymbol, which is Roslyn's concept of a type. If you want to ask "is it an int" there's also a .SpecialType property on ITypeSymbol which gives you an enum for some common cases.

bonus points if you can explain to me what the "SemanticModel" is and how it differs from a syntax tree.

A syntax tree is just syntax, that's it. Give us some text, we know what the tree is. The semantic model lets you ask questions about trees in a compilation, where the context is all of the trees plus references together.

var targets = compilation.SyntaxTrees

You'll want to look at ISyntaxReciever for a way to walk trees that'll be a bit friendlier to our performance since we're better scheduling when stuff happens.

like image 82
Jason Malinowski Avatar answered May 25 '26 18:05

Jason Malinowski