LoginSignup
8
15

More than 1 year has passed since last update.

【C#】ソースジェネレータを使ってみる

Posted at

C# 9.0からSource Generatorという機能が追加されています。
ざっくり言えば「ビルド時、コンパイル前にプログラムを生成するプログラムを実行する」機能です。
今回はGitの情報をプログラムに埋めてみます。

Source Generatorを開発するための準備

Visual Studioに追加コンポーネントを入れる

標準では入っていないのでVisual Studio Installerから追加します。
.NET Compiler Platform SDKをインストールします。
image.png

・・・これコマンドでできないんですかね?
アップデートとかも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の情報を取りましたが、色々とつかいみちがありそうです。
そのうちソースジェネレータのデバッグ方法を記事にしたいと思います。

8
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
15