Files
WpfExtended/WpfExtended.SourceGeneration/DependencyPropertyGenerator.cs
T
Alexandru Macocian 7d3c2fbb9b Alpha implementation of dependency property source generator.
Testing project for source generator
2021-04-06 10:40:06 +02:00

187 lines
8.3 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Extensions.Templates;
namespace System.Extensions
{
[Generator]
public class DependencyPropertyGenerator : ISourceGenerator
{
private const string AttributeNamespace = "System.Windows.Extensions";
private const string AttributeName = "GenerateDependencyPropertyAttribute";
private const string DependencyPropertySuffix = "Property";
private static readonly ClassTemplate GenerateDependencyPropertyAttribute =
new ClassTemplate()
.WithUsings(
new UsingsTemplate()
.WithNamespace("System"))
.WithNamespace(
new NamespaceTemplate()
.WithNamespace(AttributeNamespace))
.WithConstructor(
new ConstructorTemplate()
.WithType(AttributeName)
.WithModifiers(Modifier.Public))
.WithAttributes(
new AttributeTemplate()
.WithAttribute("AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)"))
.WithModifiers(Modifier.Public, Modifier.Sealed)
.WithName(AttributeName)
.WithBase("Attribute")
.WithProperty(
new PropertyTemplate()
.WithModifiers(Modifier.Public)
.WithName("InitialValue")
.WithType("object"));
public void Initialize(GeneratorInitializationContext context)
{
// Register a syntax receiver that will be created for each generation pass
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
var attributeText = GenerateDependencyPropertyAttribute.GenerateString();
context.AddSource("GenerateDependencyPropertyAttribute.g", attributeText);
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
return;
var options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));
var attributeSymbol = compilation.GetTypeByMetadataName($"{AttributeNamespace}.{AttributeName}");
if (attributeSymbol is null)
{
return;
}
var fieldSymbols = new List<IFieldSymbol>();
foreach (var field in receiver.CandidateFields)
{
var model = compilation.GetSemanticModel(field.SyntaxTree);
foreach (var variable in field.Declaration.Variables)
{
var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
{
fieldSymbols.Add(fieldSymbol);
}
}
}
foreach (var group in fieldSymbols.GroupBy(f => f.ContainingType, SymbolEqualityComparer.IncludeNullability))
{
var classSource = ProcessClass((INamedTypeSymbol)group.Key, attributeSymbol, group.ToList());
context.AddSource($"{group.Key.Name}.DependencyPropertyGenerator.g.cs", classSource);
}
}
private static string ProcessClass(INamedTypeSymbol classSymbol, INamedTypeSymbol attributeSymbol, List<IFieldSymbol> fields)
{
if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
{
return null; //TODO: issue a diagnostic that it must be top level
}
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var source = new ClassTemplate()
.WithUsings(
new UsingsTemplate()
.WithNamespace("System"),
new UsingsTemplate()
.WithNamespace("System.Windows"))
.WithNamespace(
new NamespaceTemplate()
.WithNamespace(namespaceName))
.WithModifiers(Modifier.Public, Modifier.Partial)
.WithName(classSymbol.Name);
foreach (var fieldSymbol in fields)
{
ProcessField(source, classSymbol, attributeSymbol, fieldSymbol);
}
return source.GenerateString();
}
private static void ProcessField(ClassTemplate source, INamedTypeSymbol classSymbol, INamedTypeSymbol attributeSymbol, IFieldSymbol fieldSymbol)
{
var fieldName = fieldSymbol.Name;
var fieldType = fieldSymbol.Type;
string propertyName = GenerateName(fieldName);
if (propertyName.Length == 0 || propertyName == fieldName)
{
//TODO: issue a diagnostic that we can't process this field
return;
}
var attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
var initialValueOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "InitialValue").Value;
var dependencyPropertyValue = initialValueOpt.Value is null ?
@$"DependencyProperty.Register(""{propertyName}"", typeof({fieldType}), typeof({classSymbol.Name}))" :
@$"DependencyProperty.Register(""{propertyName}"", typeof({fieldType}), typeof({classSymbol.Name}), new PropertyMetadata({initialValueOpt.ToCSharpString()}))";
source
.WithField(
new FieldTemplate()
.WithModifiers(Modifier.Public, Modifier.Static, Modifier.Readonly)
.WithType("DependencyProperty")
.WithName(propertyName + DependencyPropertySuffix)
.WithValue(dependencyPropertyValue))
.WithProperty(
new PropertyTemplate()
.WithType(fieldType.ToString())
.WithName(propertyName)
.WithGetter(
new GetterTemplate()
.WithCode(
new CodeBlockTemplate()
.WithLine($"this.{fieldName} = ({fieldType})this.GetValue({propertyName + DependencyPropertySuffix});")
.WithLine($"return ({fieldType})this.GetValue({propertyName + DependencyPropertySuffix});")))
.WithSetter(
new SetterTemplate()
.WithCode(
new CodeBlockTemplate()
.WithLine($"this.{fieldName} = value;")
.WithLine($"this.SetValue({propertyName + DependencyPropertySuffix}, value);"))));
}
private static string GenerateName(string fieldName)
{
fieldName = fieldName.TrimStart('_');
if (char.IsUpper(fieldName.First()))
{
return char.ToLower(fieldName.First()) + fieldName.Substring(1);
}
else
{
return char.ToUpper(fieldName.First()) + fieldName.Substring(1);
}
}
internal class SyntaxReceiver : ISyntaxReceiver
{
public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax &&
fieldDeclarationSyntax.AttributeLists.Count > 0)
{
this.CandidateFields.Add(fieldDeclarationSyntax);
}
}
}
}
}