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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With