C# How to Write a Source Generator Part 3/5: Coding

Kafka Wanna Fly
9 min readDec 15, 2022

--

We will implement IIncrementalGenerator so that it generates our desired code.

The Syntax Tree

IIncrementalGenerator only have one method needed to implement, the BindablePropSG.cs should look like this:

namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{

var fieldGroups = context.SyntaxProvider
.CreateSyntaxProvider(
// A function which tells what piece of code that we interest
// For every node in the syntax tree, it will apply this function
// to determine if the syntax node will be processed in Transform stage
predicate: IsBindableProp,
// Transform is a function that receives outputs from predicate stage
// Its inputs are accepted nodes from last stage
// In this stage, we just need to extract all necessary information
// to generate the code (field name, data-type, etc.)
transform: Transform
)
.Where(item => item is not (null, null))
.Collect();

// fieldGroups are all the fields (their information) that have BindableProp attribute
// Execute is a function. It will generate code from fieldGroups
context.RegisterSourceOutput(fieldGroups, Execute);
}
}
}

Before going any further, let me briefly explain what happens before our generator runs.

Generator and Compiler

The compiler will parse the source code file into a syntax tree then pass it to our generator. The generator then analyzes and creates new code. Then, give them back to the compiler.

Syntax Tree of a C# file

I’m using Visual Studio 2022. If you can’t find Syntax Visualizer window, please check out Visual Installer to install .NET Compiler Platform SDK.

.NET Compiler Platform SDK is about 2MB

Predicate, Transform and Execute

I hope the above part would give you an idea of what is going on. Let me show how I implement the all the functions in detail.

// In BindableProps project, BindableProp.cs:
namespace BindableProps
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class BindableProp : Attribute
{
public int DefaultBindingMode { get; set; }

public string ValidateValueDelegate { get; set; }

public string PropertyChangedDelegate { get; set; }

public string PropertyChangingDelegate { get; set; }

public string CoerceValueDelegate { get; set; }

public string CreateDefaultValueDelegate { get; set; }


public BindableProp()
{

}
}
}
// BindablePropsSG/Generators/BindablePropSG.cs

namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}

// We look for the code that's marked with BindableProp attribute
private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
if (node is not AttributeSyntax attributeSyntax)
{
return false;
}

var name = SyntaxUtil.ExtractName(attributeSyntax?.Name);

return name is "BindableProp" or "BindablePropAttribute";
}
}
}
The BindableProp attribute in the Syntax Tree
// In BindablePropsSG project, Utils/SyntaxUtil.cs

namespace BindablePropsSG.Utils
{
public class SyntaxUtil
{
public static string? ExtractName(NameSyntax? name)
{
return name switch
{
SimpleNameSyntax ins => ins.Identifier.Text,
QualifiedNameSyntax qns => qns.Right.Identifier.Text,
_ => null
};
}
}
}

Now we have all BindableProp syntax nodes in hand. Let’s extract necessary information.

namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}

private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}

// Notice, we return a tuple
// For those who don't know what tuple is,
// just think we return an array with 2 items
private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// The context object holds many things,
// you may debug and explore what is fit for your need
// In my case, FieldDeclarationSyntax and IFieldSymbol are OK

// This is the syntax node filtered from IsBindableProp
var attributeSyntax = (AttributeSyntax)context.Node;

// Attribute --> AttributeList --> Field
// Check out the Syntax Tree image above and you will see
if (attributeSyntax.Parent?.Parent is not FieldDeclarationSyntax fieldSyntax)
return (null, null);

var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(fieldSyntax.Declaration.Variables.FirstOrDefault()!) as IFieldSymbol;

return (fieldSyntax, fieldSymbol);
}
}
}

We want generate code for BindableProperty.Create function. It requires:

  • Name of the field
  • Its data-type
  • Name of the class
  • Default value
  • Optionally, binding mode and some more delegate functions

For required parameters, FieldDeclarationSyntax and IFieldSymbol is more than enough. For optional parameters, we would let dev input them into the properties of BindableProp.

// This is an example when dev uses all the settings

public partial class TextInput : ContentView
{
// Create prop with a few settings
[BindableProp(DefaultBindingMode = ((int)BindingMode.TwoWay))]
string text = "From every time";

// Full setting
[BindableProp(
DefaultBindingMode = ((int)BindingMode.OneWay),
ValidateValueDelegate = nameof(ValidateValue),
PropertyChangedDelegate = nameof(PropertyChangedDelegate),
PropertyChangingDelegate = nameof(PropertyChangingDelegate),
CoerceValueDelegate = nameof(CoerceValueDelegate),
CreateDefaultValueDelegate = nameof(CreateDefaultValueDelegate)
)]
string placeHolder = "Always!";

static bool ValidateValue(BindableObject bindable, object value)
{
return true;
}

static void PropertyChangedDelegate(BindableObject bindable, object oldValue, object newValue)
{
// Do something
}

static void PropertyChangingDelegate(BindableObject bindable, object oldValue, object newValue)
{
// Do something
}

static object CoerceValueDelegate(BindableObject bindable, object value)
{
// Do something
return 0;
}

static object CreateDefaultValueDelegate(BindableObject bindable)
{
// Do something
return string.Empty;
}
}

We have collected enough ingredients. Now, let’s put them altogether.

namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}

private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}

private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// ...
}

private void Execute(SourceProductionContext context, ImmutableArray<(FieldDeclarationSyntax?, IFieldSymbol?)> fieldSyntaxesAndSymbols)
{
// After Transform, we have a list of tuple (FieldDeclarationSyntax?, IFieldSymbol?)

if (fieldSyntaxesAndSymbols.IsDefaultOrEmpty)
return;

// Those fields may come from different or same class
// We need to group them by class
var groupList = fieldSyntaxesAndSymbols.GroupBy<(FieldDeclarationSyntax, IFieldSymbol), ClassDeclarationSyntax>(
fieldGroup => (ClassDeclarationSyntax)fieldGroup.Item1!.Parent!
);

// I'm sorry if the above code cause you a headache
// It was months ago and I feel the same when writing this story

// All you need to know is that
// groupList is a list of map
// and it has shape similar to this:
// [
// { classDeclaration1, [tuple1, tuple2, ...] },
// { classDeclaration2, [tuple1, tuple2, ...] },
// ...
// ]
foreach (var group in groupList)
{
string sourceCode = ProcessClass(group.Key, group.ToList());
// The full name includes namespace and class name
// E.g. MyMauiProject.Controls.Textlnput
var className = SyntaxUtil.GetClassFullname(group.Key);

// Notice, the output file should contain 'g' letter
// So that your IDE might know that is a generated code
context.AddSource($"{className}.g.cs", sourceCode);
}
}
}
}
// In BindablePropsSG project, Utils/SyntaxUtil.cs

namespace BindablePropsSG.Utils
{
public class SyntaxUtil
{
public static string? ExtractName(NameSyntax? name)
{
// ...
}

public static string GetClassFullname(TypeDeclarationSyntax source)
{
var namespaces = new LinkedList<BaseNamespaceDeclarationSyntax>();
var types = new LinkedList<TypeDeclarationSyntax>();

for (var parent = source.Parent; parent is object; parent = parent.Parent)
{
if (parent is BaseNamespaceDeclarationSyntax @namespace)
{
namespaces.AddFirst(@namespace);
}
else if (parent is TypeDeclarationSyntax type)
{
types.AddFirst(type);
}
}

var result = new StringBuilder();

for (var item = namespaces.First; item is object; item = item.Next)
{
result.Append(item.Value.Name).Append(".");
}

for (var item = types.First; item is object; item = item.Next)
{
var type = item.Value;
AppendName(result, type);
result.Append(".");
}

AppendName(result, source);

return result.ToString();
}

static void AppendName(StringBuilder builder, TypeDeclarationSyntax type)
{
builder.Append(type.Identifier.Text);
var typeArguments = type.TypeParameterList?.ChildNodes()
.Count(node => node is TypeParameterSyntax) ?? 0;
if (typeArguments != 0)
builder.Append(".").Append(typeArguments);
}

public static SyntaxNode? FindSyntaxBySymbol(SyntaxNode syntaxNode, ISymbol symbol)
{
var span = symbol.Locations.FirstOrDefault()!.SourceSpan;
var syntax = syntaxNode.FindNode(span);

return syntax;
}
}
}
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}

private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}

private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// ...
}

private void Execute(SourceProductionContext context, ImmutableArray<(FieldDeclarationSyntax?, IFieldSymbol?)> fieldSyntaxesAndSymbols)
{
// ...
}

private string ProcessClass(ClassDeclarationSyntax classSyntax, List<(FieldDeclarationSyntax, IFieldSymbol)> fieldGroup)
{
if (classSyntax is null)
{
return string.Empty;
}

// Import all the packages
var usingDirectives = classSyntax.SyntaxTree.GetCompilationUnitRoot().Usings;

// If class doesn't have a namespace, we'll use 'global'
var namespaceSyntax = classSyntax.Parent as BaseNamespaceDeclarationSyntax;
var namespaceName = namespaceSyntax?.Name?.ToString() ?? "global";

// Put things together
// We done the class part
var source = new StringBuilder($@"
// <auto-generated/>
{usingDirectives}

namespace {namespaceName}
{{
public partial class {classSyntax.Identifier}
{{
");

// Let go inside the field part
// Create properties for each field
foreach (var (fieldSynTax, fieldSymbol) in fieldGroup)
{
ProcessField(source, classSyntax, fieldSynTax, fieldSymbol);
}

// Don't forget the closing brackets
source.Append(@$"
}}
}}
");
return source.ToString();
}
}
}
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}

private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}

private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// ...
}

private void Execute(SourceProductionContext context, ImmutableArray<(FieldDeclarationSyntax?, IFieldSymbol?)> fieldSyntaxesAndSymbols)
{
// ...
}

private string ProcessClass(ClassDeclarationSyntax classSyntax, List<(FieldDeclarationSyntax, IFieldSymbol)> fieldGroup)
{
// ...
}

private void ProcessField(StringBuilder source, ClassDeclarationSyntax classSyntax, FieldDeclarationSyntax fieldSyntax, IFieldSymbol fieldSymbol)
{
var fieldName = fieldSymbol.Name;
// Naming convention in C#,
// field name is camel case, property name is pascal case
// StringUtil.PascalCaseOf lives in Utils/StringUtil.cs. I will show it later on
var propName = StringUtil.PascalCaseOf(fieldName);

if (propName.Length == 0 || propName == fieldName)
{
return;
}

var fieldType = fieldSyntax.Declaration.Type;

var className = classSyntax.Identifier;

var defaultFieldValue = GetFieldDefaultValue(fieldSyntax) ?? "default";

var attributeSyntax = GetAttributeByName(fieldSyntax, "BindableProp");

var attributeArguments = attributeSyntax?.ArgumentList?.Arguments;

// We're getting optional parameters
var defaultBindingMode = GetAttributeParam(attributeArguments, "DefaultBindingMode") ?? "0";

var validateValueDelegate = GetAttributeParam(attributeArguments, "ValidateValueDelegate") ?? "null";

// If there's no PropertyChangedDelegate
// We gives a default one
// This would help the prop works with MVVM
var propertyChangedDelegate = GetAttributeParam(
attributeArguments, "PropertyChangedDelegate"
) ?? @$"(bindable, oldValue, newValue) =>
(({className})bindable).{propName} = ({fieldType})newValue";

var propertyChangingDelegate = GetAttributeParam(attributeArguments, "PropertyChangingDelegate") ?? "null";

var coerceValueDelegate = GetAttributeParam(attributeArguments, "CoerceValueDelegate") ?? "null";

var createDefaultValueDelegate = GetAttributeParam(attributeArguments, "CreateDefaultValueDelegate") ?? "null";

// After retriving all required and optional parameters
// we just simply write the new source code
source.Append($@"
public static readonly BindableProperty {propName}Property = BindableProperty.Create(
nameof({propName}),
typeof({fieldType}),
typeof({className}),
{defaultFieldValue},
(BindingMode){defaultBindingMode},
{validateValueDelegate},
{propertyChangedDelegate},
{propertyChangingDelegate},
{coerceValueDelegate},
{createDefaultValueDelegate}
);

public {fieldType} {propName}
{{
get => {fieldName};
set
{{
OnPropertyChanging(nameof({propName}));

{fieldName} = value;
SetValue({className}.{propName}Property, {fieldName});

OnPropertyChanged(nameof({propName}));
}}
}}
");
}

string? GetAttributeParam(SeparatedSyntaxList<AttributeArgumentSyntax>? attributeArguments, string paramName)
{
var paramSyntax = attributeArguments?.FirstOrDefault(
attrArg => attrArg?.NameEquals?.Name.Identifier.Text == paramName
);

if (paramSyntax?.Expression is InvocationExpressionSyntax invocationExpressionSyntax)
{
return invocationExpressionSyntax.ArgumentList.Arguments.FirstOrDefault()?.ToString();
}
else if (paramSyntax?.Expression is LiteralExpressionSyntax literalExpressionSyntax)
{
return literalExpressionSyntax.Token.Value?.ToString();
}

return paramSyntax?.Expression.ToString();
}

string? GetFieldDefaultValue(FieldDeclarationSyntax fieldSyntax)
{
var variableDeclaration = fieldSyntax.DescendantNodesAndSelf()
.OfType<VariableDeclarationSyntax>()
.FirstOrDefault();
var variableDeclarator = variableDeclaration?.Variables.FirstOrDefault();
var initializer = variableDeclarator?.Initializer;
return initializer?.Value?.ToString();
}

AttributeSyntax? GetAttributeByName(FieldDeclarationSyntax fieldSyntax, string attributeName)
{
var attributeSyntax = fieldSyntax.AttributeLists
.FirstOrDefault(attrList =>
{
var attr = attrList.Attributes.FirstOrDefault();
return attr is not null && SyntaxUtil.ExtractName(attr.Name) == attributeName;
})
?.Attributes
.FirstOrDefault();

return attributeSyntax;
}
}
}
// Utils/StringUtil.cs

namespace BindablePropsSG.Utils
{
public class StringUtil
{
public static string PascalCaseOf(string fieldName)
{
fieldName = fieldName.TrimStart('_');
if (fieldName.Length == 0)
return string.Empty;

if (fieldName.Length == 1)
return fieldName.ToUpper();

return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
}
}
}

Recap

What an amount of code! I have tried to write them in a readable way. However, there might be still many exotic classes and methods you never heard. So I summarize what the code is all about.

  1. Traverse throw the Syntax Tree, find the BindableProp attribute node
  2. From the node we found, get the FieldDeclarationSyntax and IFieldSymbol
  3. Group them based on class they belong to
  4. Generating the code we need and save them to file
How it looks like when being used in a MAUI project

You can try to run or debug code from the UnitTest project.

--

--