# SourceGeneratorのサンプル(AutoNotify)の個人的な学習メモ ## ソースコード https://github.com/dotnet/roslyn-sdk/blob/main/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs ## SyntaxReceiver ```csharp= /// <summary> /// Created on demand before each generation pass /// </summary> class SyntaxReceiver : ISyntaxContextReceiver { public List<IFieldSymbol> Fields { get; } = new List<IFieldSymbol>(); /// <summary> /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation /// </summary> public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { // any field with at least one attribute is a candidate for property generation if (context.Node is FieldDeclarationSyntax fieldDeclarationSyntax && fieldDeclarationSyntax.AttributeLists.Count > 0) { foreach (VariableDeclaratorSyntax variable in fieldDeclarationSyntax.Declaration.Variables) { // Get the symbol being declared by the field, and keep it if its annotated IFieldSymbol fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol; if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.ToDisplayString() == "AutoNotify.AutoNotifyAttribute")) { Fields.Add(fieldSymbol); } } } } } ``` ### ISyntaxContextReceiver これを実装して登録しておくと、コンパイラがソースコードから構文木を作り終えたところでOnVisitSyntaxNodeを呼び出してくれる。 ### GeneratorSyntaxContext 構文情報など。 ### ○○Syntax FieldDeclarationSyntax:フィールド宣言 MethodDeclarationSyntax: メソッド宣言 ClassDeclarationSyntax: クラス宣言 NamespaceDeclarationSyntax: 名前空間宣言 PropertyDeclarationSyntax: プロパティ宣言 LocalDeclarationStatementSyntax: ローカル変数の宣言 InvocationExpressionSyntax: メソッド IfStatementSyntax: if文 ForStatementSyntax、WhileStatementSyntax: ループ文 ### fieldDeclarationSyntax.Declaration.Variables FieldDeclarationSyntaxのプロパティ 宣言している変数を複数個返す。 ```csharp= int a,b;//これならa,bが返る。 ``` ### SemanticModel 構文木と詳しい情報 ### ISymbol.GetAttributes() シンボルから属性をリストで返す。 そのあとに、AutoNotify.AutoNotifyAttributeを所持していたらFieldsに追加している。 ## GeneratorInitializationContext Initializeで使用される。 ジェネレーターがどのように動作するかを設定するためのメソッドやプロパティを提供。 ## Initialize ```csharp= public void Initialize(GeneratorInitializationContext context) { // Register the attribute source context.RegisterForPostInitialization((i) => i.AddSource("AutoNotifyAttribute.g.cs", attributeText)); // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } ``` 生成前に実行される。 ### RegisterForPostInitialization ソースコードを生成(追加)。 ### RegisterForSyntaxNotifications 生成毎にGeneratorPostInitializationContextを作成するためのActionを登録。 ```csh= (引数(Action<GeneratorPostInitializationContext> callback)) ``` ### Group化 ```csharp= foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in receiver.Fields.GroupBy<IFieldSymbol, INamedTypeSymbol>(f => f.ContainingType, SymbolEqualityComparer.Default)) { string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); context.AddSource($"{group.Key.Name}_autoNotify.g.cs", SourceText.From(classSource, Encoding.UTF8)); } ``` 例えば、 ```csharp= class A{ int x,y; } ``` があったら、AがKeyでx,yがValue。 クラスごとに、どれだけ対象のフィールドがあるかグループ化している。 ## ProcessClass ```csharp= private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, GeneratorExecutionContext context) { if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) { return null; //TODO: issue a diagnostic that it must be top level } string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); // begin building the generated source StringBuilder source = new StringBuilder($@" namespace {namespaceName} {{ public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} {{ "); // if the class doesn't implement INotifyPropertyChanged already, add it if (!classSymbol.Interfaces.Contains(notifySymbol, SymbolEqualityComparer.Default)) { source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); } // create properties for each field foreach (IFieldSymbol fieldSymbol in fields) { ProcessField(source, fieldSymbol, attributeSymbol); } source.Append("} }"); return source.ToString(); } ``` ### ContainingSymbol 一つ上の親のシンボル。 ```csharp= if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) ``` つまりこの条件式は、一つ上の階層がネームスペースじゃなかったらnullを返している。 →ネストしていた場合は、生成しない。 →TODOは、ネストしていた場合に警告を出さないといけないことを示す。 ```csharp= //TODO例 var descriptor = new DiagnosticDescriptor( "MY001", // 警告ID "Class must be top level", // タイトル "Class '{0}' must be a top-level class", // メッセージフォーマット "Usage", // カテゴリ DiagnosticSeverity.Error, // 重大度レベル true, // 有効化 "A class is nested inside another class or structure"// 説明 ); var diagnostic = Diagnostic.Create(descriptor, classSymbol.Locations[0], classSymbol.Name); context.ReportDiagnostic(diagnostic); ``` ### インターフェースが実装済みか ```cshar= if (!classSymbol.Interfaces.Contains(notifySymbol, SymbolEqualityComparer.Default)) { source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); } ``` 既にSystem.ComponentModel.PropertyChangedEventHandlerが実装されているか見る。 ## ProcessField ```csharp= private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) { // get the name and type of the field string fieldName = fieldSymbol.Name; ITypeSymbol fieldType = fieldSymbol.Type; // get the AutoNotify attribute from the field, and any associated data AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; string propertyName = chooseName(fieldName, overridenNameOpt); if (propertyName.Length == 0 || propertyName == fieldName) { //TODO: issue a diagnostic that we can't process this field return; } source.Append($@" public {fieldType} {propertyName} {{ get {{ return this.{fieldName}; }} set {{ this.{fieldName} = value; this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName}))); }} }} "); string chooseName(string fieldName2, TypedConstant overridenNameOpt2) { if (!overridenNameOpt2.IsNull) { return overridenNameOpt2.Value.ToString(); } fieldName2 = fieldName2.TrimStart('_'); if (fieldName2.Length == 0) return string.Empty; if (fieldName2.Length == 1) return fieldName2.ToUpper(); return fieldName2.Substring(0, 1).ToUpper() + fieldName2.Substring(1); } } ``` ### 属性の名前付き引数(ぷろぱてぃ)の取得 ```csharp= // get the AutoNotify attribute from the field, and any associated data AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; ``` AttributeData.NamedArguments : 名前付き引数でーた AttributeData.ConstructorArguments : コンストラクタの引数でーた ### 参考にさせていただいたものたち https://github.com/dotnet/roslyn-sdk/blob/main/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs https://zenn.dev/shimat/articles/5b81d6627491ab https://blog.masuqat.net/2015/12/23/introduction-of-roslyn-semantic-model/