Open27

SourceGeneratorを.NET6で使う

M SugiuraM Sugiura

SourceGenratorプロジェクト自身は net standard 2.0 で作成する制限が存在する。
SourceGenratorを参照するプログラムはNET6でも構わない。

未確定だが、SourceGenratorが生成するコードは単なるテキストであるため、
.NET6を前提としたコードを生成することはできると思われる。

M SugiuraM Sugiura

記事通りに進める。

[Generator]
public class DemoSourceGenerator : IIncrementalGenerator
{
   public void Initialize(IncrementalGeneratorInitializationContext context)
   {
      throw new Exception("Test exception!"); // delete me after test
   }
}

このコードが上手く認識しない。
正確に言うと、最初は認識していたのだが、CS8032が出たので、色々プロジェクトファイルやらNugetパッケージやらいじっていたら、CS8032もでなくなったが、"Test exception!"の警告も出なくなった。

この辺、VS上でのフォローがいまいち(エディタでプロジェクトファイルを触らなければいけない)なので、足踏み中。
この問題を整理してから進まないと手戻りしそうだ。

M SugiuraM Sugiura

何もしていない(?)のにCS8784が出てきた。
これは期待する警告なので、正常にSourceGeneratorが動いていると言える。

だが、何もしてないぞ?
正確に言うと再コンパイルをしただけだ。
さっきから再コンパイルはしているけど、急に反応し始めた。
なんか不安定だな・・・

M SugiuraM Sugiura

SourceGeneratorと連携ができたので、一旦ステージングしつつ、作業内容をまとめる。

・SourceGeneratorプロジェクト
プロジェクトファイルは現在この様になっている

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>10.0</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Class1.cs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.7.0" PrivateAssets="all" />
  </ItemGroup>
  
  <PropertyGroup>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>
</Project>

<TargetFramework>netstandard2.0</TargetFramework>でターゲットフレームワークが「netstandard2.0」であることに注意されたし。
これは新規にプロジェクトを作成するときに選択すればいいだけなので、大したことではない。

<LangVersion>10.0</LangVersion>で文法をC#10にしている。namesacpeでインデントつけるのはもう生理的に嫌なのでつけた。多分必須ではない。

<Nullable>enable</Nullable>、<ImplicitUsings>enable</ImplicitUsings>は記事にあるとおりに設定で、何も考えずにつけた。
必須ではないと思う。

パッケージ「Microsoft.CodeAnalysis.CSharp.Workspaces」はこれと言って用途がわかっていない。
「Microsoft.CodeAnalysis.CSharp」がSourceGeneratorであることは知っているが、「Workspaces」とはなんなのか。一旦無視する。

パッケージ「Microsoft.CodeAnalysis.Analyzers」もSourceGeneratorであることは知っている。ただ、
CS8032の回避策として追加してみた。
(詳細は以下のURLを参考されたし。 https://zenn.dev/mayuki/articles/c4728ae9cdef8e)
同記事にもあるがバージョンが3.X系とかの話である。
私が使っている Microsoft.CodeAnalysis.CSharp.Workspaces は 4.X であり、この記事の症例とは違う気がする・・・
試しに、「Microsoft.CodeAnalysis.Analyzers」を削除し、再コンパイルをかけてみたが、CS8032はでなかった。え?どういうこった・・・

<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>はVSが警告でRS1036を出してくるので、付けた。
SourceGeneratorプロジェクトに付ける必要があるので注意。
(SourceGeneratorプロジェクトを参照するプロジェクトではない)

M SugiuraM Sugiura

上記の通り、挙動が不安定なのでVSを再起動して再コンパイルをしてみる。
警告はこちらが出ている。期待通りである。

重大度レベル	コード	説明	プロジェクト	ファイル	行	抑制状態
警告	CS8784	ジェネレーター 'DemoSourceGenerator' を初期化できませんでした。出力には寄与しません。結果として、コンパイル エラーが発生する可能性があります。例外の型: 'Exception'。メッセージ: 'Test exception!'	CarbunqlConsole	D:\Local\git_workspace\Carbunql\demo\CarbunqlConsole\CSC	1	アクティブ

ということで最新のSourceGeneratorプロジェクトファイルの中身はこうだ

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>10.0</LangVersion>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <Compile Remove="Class1.cs" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.7.0" PrivateAssets="all" />
  </ItemGroup>
  
  <PropertyGroup>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>
</Project>

<Compile Remove="Class1.cs" />とかいうゴミがついてしまっているが、気にしないでくれ。

M SugiuraM Sugiura

次にSourceGeneratorを使う側のプロジェクトファイルを見て見る。

<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net6.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

	<ItemGroup>
		<ProjectReference Include="..\..\src\Carbunql.SourceGenerator\Carbunql.SourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
		<ProjectReference Include="..\..\src\Carbunql\Carbunql.csproj" />
	</ItemGroup>

</Project>

SourceGeneratorプロジェクト参照に

ReferenceOutputAssembly="false" OutputItemType="Analyzer"

という設定を付けている。
とりあえず付けろと言われているので付けている。
もちろん意味はあるんだろうが、細かいことは後回しだ。

M SugiuraM Sugiura

ステージングにしたので、この設定を削ってみよう。

ReferenceOutputAssembly="false" OutputItemType="Analyzer"

すると、CS8784警告は出なくなってしまった。つまり、SourceGeneratorが動いていないと言える。
では元記事に戻ってみよう。

・ReferenceOutputAssembly="false": we don’t need the DemoSourceGenerator.dll in the folder bin of the DemoConsoleApplication
・OutputItemType="Analyzer": the Source Generators are packaged and deployed the same way as the Roslyn Analyzers

1行目について。
SourceGeneratorはコンパイル時には必要があるが、実行時には必要がない。よって、dllを出力する必要はない、って意味だ。この設定が漏れても動くには動くと思う。ゴミだから出力しないのが望ましいが。

2行目について。
アナライザとして扱えって意味だと思う。
同設定をしない場合、アナライザはこのようになっている。

設定するとこうなる。(アナライザが1行増えてる)

余談だが、ちょっと前まで、アナライザのSourceGeneratorのところに赤いマークがついていた。
参照がうまく行ってないのか、すっごい気になっていたが、いつの間にか消えていた。
VSを再起動したタイミング(プロジェクトファイルを読み込み直したタイミング)だろうか。

プロジェクトファイルを直編集しているので、こういうこともあるだろう。
気になったらVS再起動してみるといいのだろう。

M SugiuraM Sugiura

ちなみに
SourceGenerator は netstandard 2.0 で、
Condoleは .NET6 で作ってあるが、うまく動いているようである。

M SugiuraM Sugiura

まだやり始めたばかりなので、引き続きスクラップは更新し続ける。

こちらの記事も参考になると思う。
https://zenn.dev/pcysl5edgo/articles/6d9be0dd99c008
内容が中級者向けなのか、完全初心者だとついていけない。

ソースコードをGithubにあげてくれているので、参考にはなる、かもしれない。
(読み込んではいない)
https://github.com/pCYSl5EDgo/EmbeddingResourceCSharp/blob/main/src/EmbedResourceCSharp.Roslyn3/Generator.cs

足がかりにはなったのでLIKEしておこう、情報ありがとうざいます

M SugiuraM Sugiura

やりたいことは、スーギ・ノウコ自治区さんの記事に似ているので、そちらを軸に設定を変更。

原因究明はあとにするが、
・プロジェクトの変更を変えたら、「CS8784」は出なくなった。
・ソースコードのジェネレートは成功した
・ただし、ジェンレート処理は初回コンパイル時に限られるようで、開発中のときはいまいちな部分もある。(完成物としては正しいと思う)

たぶん、初回しか生成されない理由はこのメソッドを使っているからだと思う。

常に生成結果が変わらないソースコードはRegisterPostInitializationOutputメソッドで出力するべきです。

補足、このコメントは完全リアルタイムかつ、個人の整理用に書いているので、書いている内容はかなり推測が入っています。
他人にとっての有用性は気にしてないし、自分の無知具合も存分に晒す。
なんせ勉強中のことだから。

M SugiuraM Sugiura

ChatGTPに聞いてみることにする。
それっぽい回答をしてくるが、こいつの情報源はなんなのだ・・・

ここでは、Execute メソッド内で生成したコードを RegisterPostInitializationOutput メソッドを使用して提供しています。コンパイルフェーズが完了した後、コードジェネレータが生成したコードがコンパイルされたアセンブリに含まれます。

この機能により、ソースジェネレータが生成したコードが、コンパイルエラーや警告を引き起こす前にコンパイラの解析フェーズを通過できるため、より柔軟で効果的なソースコードの生成と統合が可能になります。

こいつの言うことを信じるには、コンパイルエラー前にコードを突っ込むという意味であり、1回しかコードを吐かないという意味はなさそうだ。

M SugiuraM Sugiura

いまのところ、SourceGeneratorの生成するコードを変更した場合、VS再起動が必要だと、そう考えることにした。

SourceGeneratorプロイジェクトをクリーンビルドしても、
SourceGeneratorを参照するプロジェクトをクリーンビルドしても、
解決はしないことはわかっている。

M SugiuraM Sugiura

IncrementalValueProviderExtensions.Select でコードを検証するようである。
しかし、このコード、適当に書いても(名前空間やクラス名を実在しないものにしても)コンパイルは通ってしまうし、コードは生成されてしまう。

うーん、設定が悪いのだろうか・・・
このことは一旦忘れる

M SugiuraM Sugiura

色々迷走している。楽しさより面倒臭さが勝ってしまいそうだ。

dotnet-how-to-debug-source-generator-vs2022
https://github.com/JoanComasFdz/dotnet-how-to-debug-source-generator-vs2022

デバッグ環境の準備でもしてみよう。
Compiler Platform SDK
なるものをインストールしないといけないらしい。
普通の人はインストールしていないと思うから、個別でインストールしておこう。

なお、本当に必要かどうかは現時点では知らない

M SugiuraM Sugiura

とりあえず記事に沿ってやり始めるぞ。
記事を全部読んでないのでゴールがどこか知らん。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

</Project>

これをこうだ。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Nullable>enable</Nullable>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
    <IsRoslynComponent>true</IsRoslynComponent>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
  </ItemGroup>

  <ItemGroup>
    <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
  </ItemGroup>

</Project>

増えたところをおさらいしよう

<Nullable>enable</Nullable>
null許容

<LangVersion>latest</LangVersion>
言語バージョン

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
わからん。

また、ビルド プロパティを設定して、生成されたファイルを保存し、生成されたファイルが格納される場所を制御することもできます。 コンソール アプリケーションのプロジェクト ファイルで、<EmitCompilerGeneratedFiles> 要素を <PropertyGroup> に追加し、その値を true に設定します。 プロジェクトをもう一度ビルドします。 これで、生成されたファイルが obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator の下に作成されます。 パスのコンポーネントは、ビルド構成、ターゲット フレームワーク、ソース ジェネレーター プロジェクト名、およびジェネレーターの完全修飾型名にマップされます。 アプリケーションのプロジェクト ファイルに <CompilerGeneratedFilesOutputPath> 要素を追加することで、より便利な出力フォルダーを選択できます。

この辺のことだとは思う。
https://learn.microsoft.com/ja-jp/dotnet/csharp/roslyn-sdk/source-generators-overview

<IsRoslynComponent>true</IsRoslynComponent>

https://qiita.com/ryuix/items/36dabbf3c7e4e395e49e
デバッガ動かすぞ、ってことみたい?

M SugiuraM Sugiura

んで

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

これをこうだ

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference
      Include="..\MySourceGenerator\MySourceGenerator.csproj"
      OutputItemType="Analyzer"
      ReferenceOutputAssembly="false"/>
  </ItemGroup>
</Project>

OutputItemType、ReferenceOutputAssembly は既に意味は知っている。
このコード?に目新しさはない

M SugiuraM Sugiura

Generatorプロジェクトの設定が見たこと無いやつもあるが、無視する。

M SugiuraM Sugiura

SourceGeneratorプロジェクトは実質空っぽならので、ブレイクする箇所もない。
というわけで例のコードを書いとく

using Microsoft.CodeAnalysis;

namespace SourceGenerator;

[Generator]
public class SourceGenerator: IIncrementalGenerator
{
   public void Initialize(IncrementalGeneratorInitializationContext context)
   {
   }
}

ここにブレークポイントおけばどうなるのか

M SugiuraM Sugiura

やりたいことを振り返りをしてみる。

1.SQLをパースしてオブジェクトにしたい。
2.解析処理をするので初回実行が重たい。遅い。
3.SourceGeneratorでコンパイル時に解析するのがいいのではないか?

1,2は実現済みだからよい。
問題は3だ。
SourceGeneratorを使えば・・・というのはアイデアとしてはありだが、
SourceGeneratorを使えば解決するのかというと、そんな単純なものではない。

仮にSelectQueryAttributeを作るとしよう。
使用イメージはこんなふうだ

public class classA
{
[SelectQuery("select * from table_a")]
public static partial SelectQuery GetQuery();
}

自動生成されるコードはどうしたらいいだろうか?

public static partial global::Carbunql.SelectQuery GetQuery()
{
    return new global::Carbunql.SelectQuery("select * from table_a");
}

こうか?そんなわけ無い!

new global::Carbunql.SelectQuery("select * from table_a");

パースが遅いからコンパイル時になんとかしたい訳で、実行時にパースするコードをコンパイル時に生成しても何の意味もない。

つまり、パースじゃない方法でインスタンスをしなきゃ意味がない。

最速は手組みだろう。
SelectQueryクラスを解析して、手組み処理の記述をするかって?
それはむちゃだろう。
それをやりたくないから、パース機能作ったんだし。

M SugiuraM Sugiura

じゃあ、どうしたらいい?無理だ!で終わらすか?

正直なところ終わらせてもいいと思う。
私が力入れたいのはここではないから。

しかしである。

バカバカしいかもしれないが、JSONにしたらどうだろうか。
SQL解析とJSON解析どっちが速いんだって話になるが、
SQLよりJSONのほうが構造簡単じゃないのか?SQLは文だし。

SQLのディープコピーにも使えるし、JSON化っていうのを試してみてもいいのでは?