# 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/