C# 9.0からSource Generatorという機能が追加されています。
ざっくり言えば「ビルド時、コンパイル前にプログラムを生成するプログラムを実行する」機能です。
今回はGitの情報をプログラムに埋めてみます。
Source Generatorを開発するための準備
Visual Studioに追加コンポーネントを入れる
標準では入っていないのでVisual Studio Installerから追加します。
.NET Compiler Platform SDK
をインストールします。
・・・これコマンドでできないんですかね?
アップデートとかもGUIでやるのめんどくさいなーと思ってます。知らないだけかな・・
プロジェクトの準備
なにはともあれ初期化します。いつものやつで、名前は「SourceGen」とします。
dotnet new sln --name SourceGen
dotnet new classlib --name SourceGen
dotnet sln add
で、csprojを編集します。
- Microsoft.CodeAnalysis.CSharpへの参照を追加
- TargetFrameworkをnetstandard2.0に
- (Visual Studioが未だに.NET Frameworkを抜け出せていないのが理由らしいです。)
- IsRoslynComponentをtrueに
- AnalyzerLanguageをcsに
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>11</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<AnalyzerLanguage>cs</AnalyzerLanguage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
</ItemGroup>
</Project>
Gitから情報取得
今回は現在のブランチ名をソースに埋めてみます。
期待する結果としては
using SourceGen;
namespace SourceGenConsoleTest;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(HogeMoge.BranchName);
}
}
[GenerateGitInformation]
public partial class HogeMoge
{
}
で、
> master
が表示できればOKです。
gitの情報を取得するのはProcess.Startで出力をいい感じに読み取ればできるので割愛します。
ソースコードの出力
SourceGeneratorはIIncrementalGenerator
をroslynが認識して実行することで動きます。
Attributeで言語も指定します。
[Generator(LanguageNames.CSharp)]
public class GitInformationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
}
}
IncrementalGeneratorInitializationContextに対してアレコレすることでソース生成をします。
まずGenerateGitInformation
を生成させます。
参照元のプロジェクトで展開されるので、アクセスレベルはinternalで良いです。
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(
static context =>
{
context.AddSource(
"GitVersionSourceGenerationAttribute.cs",
"""
namespace SourceGen;
using System;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal sealed class GenerateGitInformationAttribute : Attribute
{
}
""");
});
}
そして、上記で追加した属性に対してソース生成を行います。
属性使わずにソース生成してもいいのですが、勝手に色々やられると厄介になりがちので、属性をつけることを起点としたほうがいいと思います。
context.SyntaxProvider.ForAttributeWithMetadataName
で該当ソースを取得できるのでまずはそうします・
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 省略
var source = context.SyntaxProvider.ForAttributeWithMetadataName(
"SourceGen.GenerateGitInformationAttribute", // 取得する属性
(_, _) => true, // trueでOK 追加でなにか条件があればここにラムダ式とかでかける
(syntaxContext, _) => syntaxContext); // 調べていません。ごめんなさい><
}
sourceが取れたら、ごにょごにょしてcontext.RegisterSourceOutput
に渡します。
(正確には、ごにょごにょするメソッドをcontext.RegisterSourceOutput
にわたす)
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 省略
var source = context.SyntaxProvider.ForAttributeWithMetadataName(
"SourceGen.GenerateGitInformationAttribute", // 取得する属性
(_, _) => true, // trueでOK 追加でなにか条件があればここにラムダ式とかでかける
(syntaxContext, _) => syntaxContext); // 調べていません。ごめんなさい><
context.RegisterSourceOutput(source, Emit);
}
private static void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
{
var typeNode = (TypeDeclarationSyntax)source.TargetNode;
var typeSymbol = (INamedTypeSymbol)source.TargetSymbol;
// 名前空間を考える。partialクラスで定義するので同じ名前空間にする
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace ? "" : $"namespace {typeSymbol.ContainingNamespace};";
// ソースを考える
var code =
$$"""
// <auto-generated/>
{{ns}}
partial class {{typeSymbol.Name}}
{
public static string BranchName => "{{GetBranchName()}}";
}
""" ;
// ファイル名に使う
var fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", "")
.Replace("<", "_")
.Replace(">", "_");
// 拡張子はg.csとする
context.AddSource($"{fullType}.GitInformationGenerator.g.cs", code);
// 割愛。
string GetBranchName() => "master";
}
デバッグ準備
ソース生成をされる側のプロジェクトを作って、先程作ったジェネレータを参照します。
<ItemGroup>
<ProjectReference Include="..\..\SourceGen\SourceGen.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
これがちゃんと動けばOKです!
using SourceGen;
namespace SourceGenConsoleTest;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(HogeMoge.BranchName);
}
}
[GenerateGitInformation]
public partial class HogeMoge
{
}
まとめ
今回はgitの情報を取りましたが、色々とつかいみちがありそうです。
そのうちソースジェネレータのデバッグ方法を記事にしたいと思います。