🍉

ソースジェネレータとInternalsVisbleToの組み合わせで発生するCS0436を緩和する

2022/08/18に公開

ソースジェネレータとInternalsVisbleToの組み合わせで発生するCS0436を緩和する

自作のソースジェネレータを使用していた時に、InternalsVisbleTo属性を使用してinternalな型などを公開している場合、参照側プロジェクトにあるソースジェネレータが生成したクラス定義と被参照側のプロジェクトにあるソースジェネレータが生成したクラス定義が競合してしまうことで、意味のないCS0436が発生する問題に悩まされたので、おなじ悩みに遭遇した人向けにこのような場合に発生するCS0436を回避する方法について記事にしてみました。
また、この方法についてなにか私が見落としている思われる問題があったり、別の方法がありましたらご指摘頂けると幸いです。

発生例

まず、実際に発生した場合の実例を示すと以下のような状況になります。

非参照側のプロジェクトのサンプル画像

参照側のプロジェクトのサンプル画像

エラー一覧に現れているCS0436

この例では、ClassLibrary1とClassLibrary2のプロジェクトがあり、ClassLibrary2はClassLibrary1を参照しています。また、ClassLibrary1はInternalsVisbleTo属性によってClassLibrary2にinternalな型やメンバを公開しています。

それぞれのクラス定義につけられているAutomaticDisposeImpl属性はソースジェネレータによって生成されたソース生成の目印として使う属性です。internalクラスなので通常は異なるアセンブリに名前空間を含めて同名で存在していても問題はありません。しかし、ClassLibrary2ではInternalsVisbleTo属性によってClassLibrary1の中で定義されているAutomaticDisposeImpl属性も一般のpublicクラスと同様に見えてしまうため、ClassLibrary1とClassLibrary2のAutomaticDisposeImplが競合してしまい、CS0436警告が発生します。

解決方法1

CS0436に対する通常行う簡潔な解決方法は2通りあります。

  1. csproj(Directory.Build.propsなどでも良し)に<NoWarn>$(NoWarn);CS0436</NoWarn>を追加する
  2. EditorConfigにdotnet_diagnostic.CS0436.severity = suggestionのようにCS0436の重要度を下げる設定を追加する

どちらも設定後はCS0436が警告として報告されなくなります。csprojを弄る場合はCS0436が跡形もなく消えますが、EditorConfigを弄る場合は上記のように完全に消すのではなく、重要度を調整して警告よりも低いレベルの報告として残すことが出来ます。

ただし、どちらの場合もCS0436がソースジェネレータの定義に由来する型の競合による報告だったのか、それ以外の本来注意すべき自分自身のソースに起因する型の競合よる報告だったのかが区別されずに一様に抑止されてしまいます。

CS0436を必要としておらず、完全になくしてしまっても良い場合はこの対処で良いと思いますが、従来の理由で発生するCS0436を捨てたくない場合は注意が必要です。

私の場合は、ソースジェネレータ起因のCS0436は無視したい一方でそれ以外のCS0436がもし報告されるとしたらそれは無視したくないという状況にあり、出来ればこの解決方法は採用したくありませんでした。

解決方法2

紆余曲折あって、アナライザを作成することで、CS0436の報告を目的に合った形で疑似的に緩和することが出来ました。CS0436を完全に抑止せずにソースジェネレータ由来の場合のみを排除したいというニーズが他にどれほどあるか分かりませんが、もし同じことにお悩みの人がいましたら役に立つのではないかと思います。

具体的には、以下のようにします。

  1. 本物のCS0436の重要度をEditorConfigでsuggestionまたはsilentに落とす
  2. アナライザによってソースジェネレータが生成したソースで定義されたシンボル以外に対してCS0436が報告されていた箇所にのみ独自のRuleIdでwarningを報告

この方法では解決方法1と同様に本物のCS0436は見えなくなりますが、別IDでCS0436と同等のwarningをソースジェネレータが生成したソースに由来する箇所を除いて受け取ることが出来るようになります。

このアナライザ自体はBenutomo.Cs0436Relaxationという名称でNuGetでも公開してあります。

Benutomo.Cs0436Relaxation

Benutomo.Cs0436Relaxationという名前で公開していますが、上述の通りこのアナライザ自身にCS0436を抑止する機能はなく、EditorConfigによって抑止(suggestionまたはsilent化)されたCS0436が報告された箇所を調査し、そのCS0436がソースジェネレータで定義された型に対するものでなければ、CS0436の代替としてRX_CS0436_1をwarningで報告します。

導入するためには、ソリューションフォルダの直下に以下のファイルを配置します。すでに該当ファイルが存在する場合は該当するファイルに以下の内容をマージします。

Directory.Build.props
<Project>
  <ItemGroup>
    <PackageReference Include="Benutomo.Cs0436Relaxation" Version="1.0.0-alpha9" PrivateAssets="true" />
  </ItemGroup>
</Project>
.editorconfig
root = true

[*.cs]
dotnet_diagnostic.CS0436.severity = silent

例えば、冒頭のサンプルのような環境に配置した場合は以下のようになります。

  • ソリューションフォルダ/
    • ClassLibrary1/
      • ClassLibrary1.csproj
      • Class1.cs
    • ClassLibrary2/
      • ClassLibrary2.csproj
      • Class1.cs
    • .editorconfig
    • Directory.Build.props
    • Solution.sln

この設定はソリューション内の全てのプロジェクトに影響するようになります。上述の設定方法にこだわらず、特定のプロジェクトにのみこの設定を適用するようにすることもできますが、お勧めはしません。

最後に注意点ですが、このアナライザはコンパイル対象として現れるソースがソースジェネレータによるものか否かの判定をRoslynコンパイラがソースに対して付与しているソースファイルのパスの命名方法に依存しています。特定の環境では違う命名をすることがあったり、将来のバージョンで現在の方法では区別が出来なくなった場合に動作しなくなってしまうことがあるかもしれません。私自身が使用しているため、出来る範囲で追従するつもりでおりますが、どうしても無理な場合は使えなくなってしまうことがあるかもしれません。そのときはご容赦願います。

そのほか

上述の解決方法では触れませんでしたが、ソースジェネレータが自作のものでプロジェクト参照の形で取り込んでいる場合は、ソースジェネレータで生成していた属性クラスなどを普通の共通ライブラリに移動するだけでも解決可能だったりします。

NuGetパッケージを参照してる場合でもStronglyTypedIdのようにオプションで共通ライブラリ方式に切り替える仕組みを持っているものもあります。念のため、そのような仕組みが提供されていないか確認してみるのも良いと思います(※)。

StronglyTypedIdの作者であるAndrew Lockさんはそれに関する記事も執筆されています。

※ 私はNuGetで公開している自作のソースジェネレータに対して自爆する形でこの問題に遭遇し、StronglyTypedIdと同じ考えにいたり要点は同じ対応を試したのですが、実行時には無意味な参照アセンブリが生まれてしまうことと、私が試した場合では<PacageReference>PrivateAssets="all"を付けてソースジェネレータを参照すると、さらにそれを参照しているプロジェクトで原因が分かりにくい問題が生じる場合があることが分かり、この方法は採用しないことにしました(解決方法2の冒頭に記載した紆余曲折です)。

Discussion