たいちょーの雑記

ぼくが3日に一度くらい雑記をかくところ

SourceGeneratorで自作例外クラスのコンストラクタを自動生成してみる

自作の例外クラス書いてますか?自分は時々書いてます。

で、雑に以下のように書きますよね。

public class FailedToHogeException : Exception {
    public FailedToHogeException(string message) : base(message) {}
}

するとSonarLintなどからこんな感じの警告もらいませんか?自分はもらいます。

よくみるやつ

これってちゃんと実装しようね!ってことなだけなんですけど、結構実装量多い…ような気がするので自動生成されると嬉しいですね。というわけでSourceGeneratorの素振りの題材として作ってみます。

実装したやつ

実装したものは以下のリポジトリにあります。

github.com

以下のように自動生成してもらいたい例外クラスに SerializableException属性を付けるだけで

namespace Example;

[SerializableException]
public partial class FailedToHogeException : Exception { }

次のようなファイルを生成してくれます

// <auto-generated>
// THIS FILE IS GENERATED BY SerializableExceptionGenerator. DO NOT EDIT IT.
// </auto-generated>

namespace Example;

[global::System.Serializable]
public partial class FailedToHogeException : global::System.Exception
{
        public FailedToHogeException() { }
        public FailedToHogeException(string message) : base(message) { }
        public FailedToHogeException(string message, global::System.Exception inner) : base(message, inner) { }
        protected FailedToHogeException(
                global::System.Runtime.Serialization.SerializationInfo info,
                global::System.Runtime.Serialization.StreamingContext context
        ) : base(info, context) { }
}

引数にオプション値を渡すことで、生成したいコンストラクタを自由に選ぶこともできます。例えば以下のように GenerateTarget.StringMessageExceptionConstructor だけ指定すると

using SerializableExceptionGenerator;

namespace Example;

[SerializableException(GenerateTarget.StringMessageExceptionConstructor)]
public partial class FailedToException : Exception { }

次のようなものが生成されます。

// <auto-generated>
// THIS FILE IS GENERATED BY SerializableExceptionGenerator. DO NOT EDIT IT.
// </auto-generated>

namespace Example;

[global::System.Serializable]
public partial class FailedToException : global::System.Exception
{
    public FailedToException(string message, global::System.Exception inner) : base(message, inner) { }
}

自前実装した残りは自動実装してくれよな!ってときに便利かもしれません

実現方法

最初に言った通りSourceGeneratorを使っています。SerializableException属性を探して実装を出力するIIncrementalGeneratorを書いたって感じです。

using System.Text;
using Microsoft.CodeAnalysis;

namespace SerializableExceptionGenerator;

[Generator(LanguageNames.CSharp)]
public class SerializableExceptionGenerator : IIncrementalGenerator
{
    // 名前は結構繰り返し使うので変数に入れとくと楽だったり楽じゃなかったりする
    private const string GeneratorAttributeName = "SerializableExceptionAttribute";
    private const string GeneratorNamespace = "SerializableExceptionGenerator";

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var source = context.SyntaxProvider.ForAttributeWithMetadataName(
            $"{GeneratorNamespace}.{GeneratorAttributeName}",
            static (_, _) => true,
            static (context, _) => context // ここでGenerateTargetの値とれそうなきがしなくもない
        );

        // コード生成は別のメソッドにしてる
        context.RegisterSourceOutput(source, Emit);

        context.RegisterPostInitializationOutput(static postInitializationContext =>
        {
            // 引数用のEnumってどこに実装するのがいいんだろう
            // 生成するコードに書くと、二重管理になってめんどくさい気がするんだよな…
            postInitializationContext.AddSource(
                $"{GeneratorAttributeName}.cs",
                $$"""
// <auto-generated>
// THIS FILE IS GENERATED BY SerializableExceptionGenerator. DO NOT EDIT IT.
// </auto-generated>

namespace {{GeneratorNamespace}} {

    [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
    internal sealed class {{GeneratorAttributeName}} : global::System.Attribute {
        public GenerateTarget Target { get; set; }

        public {{GeneratorAttributeName}}(GenerateTarget target = GenerateTarget.All) {
            this.Target = target;
        }
    }


    [global::System.Flags]
    internal enum GenerateTarget
    {
        None = 0,
        DefaultConstructor = 1,
        StringMessageConstructor = 1 << 1,
        StringMessageExceptionConstructor = 1 << 2,
        SerializationInfoStreamingContextConstructor = 1 << 3,
        All =
            DefaultConstructor
            | StringMessageConstructor
            | StringMessageExceptionConstructor
            | SerializationInfoStreamingContextConstructor
    }
}
"""
            );
        });
    }

    private static void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
    {
        <省略だよ>
    }

    // さっきも書いたけど生成コード中に同じものがあるのつらいお気持ちがなくはない
    [Flags]
    internal enum GenerateTarget
    {
        <省略だよ>
    }
}

コメント内にも書いていますが、オプション値として使うenumの定義が2か所に生えてしまうのをどうにかしたいなというお気持ちです。なんかいいアイデアがあれば教えてください。

まとめ

  • 自作例外クラスを書くときによく見るRSPEC-3925に対応できるSourceGeneratorが欲しかった

  • なので指定したコンストラクタを自動生成してくれるSourceGeneratorを書いた

  • オプションで生成するコンストラクタを制御できるようにした

  • フラグのenumをどこに書くべきかはいいアイデアが出なかった

正直テンプレートでもいいので使うかは謎…ですね。Copilotが勝手に埋めてくれたりもしますしね…。
とはいえ良いSourceGeneratorの素振りにはなりました。皆さんも何か書いてみてはいかがでしょうか。

| ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄|
|        終        |
|    制作・著作    |
|   ̄ ̄ ̄ ̄ ̄ ̄ ̄  |
|    xztaityozx    |
|_________|
  ∧∧   ||
  ( ゚д゚)||
  /    づΦ