10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コーディング規約に違反したらコンパイルエラーにしたい

Last updated at Posted at 2025-12-23

コーディング規約に違反した箇所を探すのがめんどくさいので、
Rider使わずにできないかなと思って試してみました。

何がしたい

  • Microsoft C#準拠 のコーディング規約に違反したらコンパイルエラーにする。
  • Unity独自のクラス(UnityEngine・UnityEditor等)は特別なコーディング規約にしたい場合、そちらの規約エラーもコンパイルエラーにする

実行環境

  • Unity6000.2.13f1

使用した.NET SDK

  • dotnet-sdk-10.0.101

手順

  • 事前に用意するもの
    1. Unityプロジェクト(既存でOK)
    2. PCに .NET SDK(dotnet コマンドが使える状態)
    3. もし無ければ「.NET SDK」をインストール(Windows/MacどちらもOK)
      .NET SDK
  • Unity側のフォルダを作る(適用範囲をAssets/Scripts内に限定したいため)
    1. Assets/Scripts を作る
      既にあればOK。
    2. Assets/Scripts に asmdef を作る(必須)
      UnityエディタでAssets/Scripts を右クリック
      Create > Scripting > Assembly Definition
      名前を Scripts にする(Scripts.asmdef ができる)
    3. Analyzer配置用フォルダを作る
      Assets/Scripts/Analyzers/ を作っておきます。

・ここまでの状態のフォルダ構成
image.png

  • .editorconfig を作る(共通ルール)

    • プロジェクトルートに .editorconfig を作成
      Assets と同じ階層(プロジェクト直下)に置きます。
      /.editorconfig(コピペOK)

      ###############################################################################
      # .editorconfig
      #
      # 役割分担:
      # - editorconfig
      #     C# 言語仕様レベルの共通ルール
      #     (public / local / property / method / parameter など)
      #
      # - Roslyn Analyzer(UnityNamingAnalyzers.dll)
      #     Unity 固有の判定が必要なルール
      #     (UnityEngine.Object / UnityEditor / private field 分岐)
      #
      # private フィールドの命名ルールは editorconfig では定義せず、
      # UVN10 / UVN11 / UVN12 として Analyzer 側で完全に管理する。
      ###############################################################################
      
      # この editorconfig をプロジェクトルートとして扱う
      root = true
      
      
      ###############################################################################
      # C# 全ファイル共通設定
      ###############################################################################
      [*.cs]
      
      # ---------------------------------------------------------------------------
      # 基本整形
      # ---------------------------------------------------------------------------
      
      # 文字コードは UTF-8 固定
      charset = utf-8
      
      # 改行コードは LF(Git / クロスプラットフォーム向け)
      end_of_line = lf
      
      # ファイル末尾に必ず改行を入れる(diff 安定化)
      insert_final_newline = true
      
      # インデントはスペース4つ(Microsoft / Unity 標準)
      indent_style = space
      indent_size = 4
      
      
      ###############################################################################
      # 命名スタイル定義
      #
      # ※ ここでは「スタイルそのもの」を定義するだけ。
      #    実際にどこへ適用するかは下の naming_rule で指定する。
      ###############################################################################
      
      # PascalCase(例: MyProperty, DoSomething)
      dotnet_naming_style.pascal.capitalization = pascal_case
      
      # camelCase(例: localValue, argValue)
      dotnet_naming_style.camel.capitalization  = camel_case
      
      
      ###############################################################################
      # public フィールド
      #
      # ルール:
      #   - public フィールドは PascalCase
      #
      ###############################################################################
      dotnet_naming_symbols.public_fields.applicable_kinds = field
      dotnet_naming_symbols.public_fields.applicable_accessibilities = public
      
      dotnet_naming_rule.public_fields_pascal.symbols = public_fields
      dotnet_naming_rule.public_fields_pascal.style = pascal
      dotnet_naming_rule.public_fields_pascal.severity = error
      
      
      ###############################################################################
      # ローカル変数
      #
      # ルール:
      #   - camelCase
      #
      ###############################################################################
      dotnet_naming_symbols.locals.applicable_kinds = local
      
      dotnet_naming_rule.locals_camel.symbols = locals
      dotnet_naming_rule.locals_camel.style = camel
      dotnet_naming_rule.locals_camel.severity = error
      
      
      ###############################################################################
      # プロパティ
      #
      # ルール:
      #   - PascalCase
      #
      ###############################################################################
      dotnet_naming_symbols.properties.applicable_kinds = property
      
      dotnet_naming_rule.properties_pascal.symbols = properties
      dotnet_naming_rule.properties_pascal.style = pascal
      dotnet_naming_rule.properties_pascal.severity = error
      
      
      ###############################################################################
      # メソッド(public / private)
      #
      # ルール:
      #   - PascalCase
      #
      ###############################################################################
      dotnet_naming_symbols.methods.applicable_kinds = method
      dotnet_naming_symbols.methods.applicable_accessibilities = public, private
      
      dotnet_naming_rule.methods_pascal.symbols = methods
      dotnet_naming_rule.methods_pascal.style = pascal
      dotnet_naming_rule.methods_pascal.severity = error
      
      
      ###############################################################################
      # 引数(parameter)
      #
      # ルール:
      #   - camelCase
      #
      ###############################################################################
      dotnet_naming_symbols.parameters.applicable_kinds = parameter
      
      dotnet_naming_rule.parameters_camel.symbols = parameters
      dotnet_naming_rule.parameters_camel.style = camel
      dotnet_naming_rule.parameters_camel.severity = error
      
      
      ###############################################################################
      # 自作 Roslyn Analyzer 診断レベル設定
      #
      # UVN10:
      #   - UnityEngine.Object 派生の private フィールド
      #   - camelCase を要求(_禁止)
      #
      # UVN11:
      #   - 非 Unity / 非 Editor 型の private フィールド
      #   - _camelCase を要求
      #
      # UVN12:
      #   - UnityEditor 型の private フィールド(Editor拡張)
      #   - _camelCase を要求(ツールコード扱い)
      #
      # ※ 現場差がある場合は warning / suggestion に落とす運用も可能
      ###############################################################################
      dotnet_diagnostic.UVN10.severity = error
      dotnet_diagnostic.UVN11.severity = error
      dotnet_diagnostic.UVN12.severity = error
      
      
  • Roslyn Analyzer を作る(今回の核心)
    ここからは Unity 外(ターミナル)で作業します。

    • Analyzerプロジェクトを作成
      任意の作業フォルダで:

      dotnet new analyzer -n UnityNamingAnalyzers
      cd UnityNamingAnalyzers
      

      私の環境ではエラーになったので、下記コマンドにてフォルダとプロジェクトを作成

      cd %USERPROFILE%\Downloads
      mkdir UnityNamingAnalyzers
      cd UnityNamingAnalyzers
      
      dotnet new classlib -n UnityNamingAnalyzers
      cd UnityNamingAnalyzers
      
    • .csproj を置き換え
      UnityNamingAnalyzers.csproj をメモ帳等で開いて、中身を全部これに置き換えて保存してください。

      <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
          <TargetFramework>netstandard2.0</TargetFramework>
          <LangVersion>latest</LangVersion>
          <Nullable>enable</Nullable>
        </PropertyGroup>
      
        <ItemGroup>
          <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
          <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
        </ItemGroup>
      </Project>
      
    • 既存の Class1.cs を削除
      このファイルは不要なので削除します:

    • Analyzerコードを追加(今回のルール)
      同じフォルダに新規ファイルを作成:
      UnitySerializeFieldNamingAnalyzer.cs

      using System;
      using System.Collections.Immutable;
      using Microsoft.CodeAnalysis;
      using Microsoft.CodeAnalysis.Diagnostics;
      
      namespace UnityNamingAnalyzers;
      
      /// <summary>
      /// Unity プロジェクト向けの命名規約を強制する Roslyn Analyzer。
      ///
      /// ■ 目的
      /// editorconfig だけでは表現しづらい「Unity 固有の型判定(UnityEngine / UnityEditor)」を行い、
      /// private フィールドの命名規約を統一する。
      ///
      /// ■ ルール(診断ID)
      /// - UVN10: UnityEngine.Object 派生(Unityの参照型)の private フィールドは camelCase(先頭小文字・_禁止)
      /// - UVN11: 上記以外(非Unity/非Editor)の private フィールドは _camelCase(_ + 先頭小文字)
      /// - UVN12: UnityEditor 型の private フィールドは _camelCase(Editor拡張は C# 寄せ)
      ///
      /// ■ 運用
      /// - エラー/警告などの厳しさは .editorconfig の dotnet_diagnostic.*.severity で調整する
      ///   例) dotnet_diagnostic.UVN10.severity = error
      /// </summary>
      [DiagnosticAnalyzer(LanguageNames.CSharp)]
      public sealed class UnityNamingAnalyzer : DiagnosticAnalyzer
      {
          // ---------------------------------------------------------------------
          // 1) 診断(DiagnosticDescriptor)定義
          // ---------------------------------------------------------------------
          // ここで「どんな違反が起きたら、どんなID/メッセージで報告するか」を定義します。
          // 重大度 (defaultSeverity) は "既定値" なので、実際の厳しさは editorconfig で上書きできます。
      
          /// <summary>
          /// UVN10:
          /// UnityEngine.Object 派生の private フィールドは camelCase(_禁止)であるべき、というルール。
          /// 例:
          ///  - OK  : private GameObject player;
          ///  - NG  : private GameObject _player;
          ///  - NG  : private GameObject Player;
          /// </summary>
          private static readonly DiagnosticDescriptor UnityPrivateCamelRule = new(
              id: "UVN10",
              title: "Unity private field must be camelCase",
              messageFormat: "UnityEngine.Object-derived private field '{0}' must be camelCase (no underscore)",
              category: "Naming",
              defaultSeverity: DiagnosticSeverity.Error,
              isEnabledByDefault: true);
      
          /// <summary>
          /// UVN11:
          /// 非 Unity / 非 Editor 型の private フィールドは _camelCase であるべき、というルール。
          /// 例:
          ///  - OK  : private int _hp;
          ///  - NG  : private int hp;
          /// </summary>
          private static readonly DiagnosticDescriptor NonUnityPrivateUnderscoreRule = new(
              id: "UVN11",
              title: "Private field must be _camelCase",
              messageFormat: "Private field '{0}' must be _camelCase",
              category: "Naming",
              defaultSeverity: DiagnosticSeverity.Error,
              isEnabledByDefault: true);
      
          /// <summary>
          /// UVN12:
          /// UnityEditor 型の private フィールドは _camelCase であるべき、というルール。
          /// 例:
          ///  - OK  : private SerializedObject _so;
          ///  - NG  : private SerializedObject so;
          /// </summary>
          private static readonly DiagnosticDescriptor UnityEditorPrivateUnderscoreRule = new(
              id: "UVN12",
              title: "UnityEditor private field must be _camelCase",
              messageFormat: "UnityEditor private field '{0}' must be _camelCase",
              category: "Naming",
              defaultSeverity: DiagnosticSeverity.Error,
              isEnabledByDefault: true);
      
          /// <summary>
          /// この Analyzer が提供する全診断一覧。
          /// Roslyn はここで列挙された診断IDのみを「このAnalyzerが出し得る」と認識します。
          /// </summary>
          public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
              => ImmutableArray.Create(UnityPrivateCamelRule, NonUnityPrivateUnderscoreRule, UnityEditorPrivateUnderscoreRule);
      
          // ---------------------------------------------------------------------
          // 2) Analyzer 登録(Initialize)
          // ---------------------------------------------------------------------
          // Roslyn へ「何を解析するか」を登録します。
          // 今回は "フィールド (SymbolKind.Field)" を対象に解析したいので、RegisterSymbolAction を使います。
      
          public override void Initialize(AnalysisContext context)
          {
              // 自動生成コード(designer.cs など)は対象外にする設定
              context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
      
              // パフォーマンス向上:並列実行を許可
              context.EnableConcurrentExecution();
      
              // フィールドシンボルが見つかるたびに AnalyzeField を呼ぶ
              context.RegisterSymbolAction(AnalyzeField, SymbolKind.Field);
          }
      
          // ---------------------------------------------------------------------
          // 3) 解析本体(AnalyzeField)
          // ---------------------------------------------------------------------
          // ここが「このAnalyzerの心臓部」です。
          // 受け取ったフィールドが対象条件に合うか判定し、命名規約違反なら診断を Report します。
      
          private static void AnalyzeField(SymbolAnalysisContext context)
          {
              var field = (IFieldSymbol)context.Symbol;
      
              // 対象は private フィールドのみ(const は除外)
              // ※ readonly を除外したい、static を除外したい等はここで条件追加できます。
              if (field.DeclaredAccessibility != Accessibility.Private) return;
              if (field.IsConst) return;
      
              // 自動実装プロパティの backing field(例: <X>k__BackingField)を除外したい場合は ON にする
              // if (field.IsImplicitlyDeclared) return;
      
              var name = field.Name;
      
              // Compilation から UnityEngine.Object 型を取得できれば "Unity参照環境" として判定可能。
              // asmdef 等で UnityEngine が参照されていない場合は null になり得る点に注意。
              var compilation = context.Compilation;
              var unityObject = compilation.GetTypeByMetadataName("UnityEngine.Object");
      
              // UnityEditor 型かどうか(Editor アセンブリでは true になりやすい)
              // UnityEditor が参照されないアセンブリでは false になります。
              bool isUnityEditorType = IsUnityEditorType(field.Type);
      
              // UnityEngine.Object 派生かどうか(Unity参照がないと unityObject が null なので false)
              bool isUnityRuntimeType = unityObject != null && InheritsFrom(field.Type, unityObject);
      
              // 優先順位(重複を避けるための設計)
              // 1) UnityEditor 型 → UVN12
              // 2) UnityEngine.Object 派生 → UVN10
              // 3) それ以外 → UVN11
              //
              // ※ UnityEditor 型が UnityEngine.Object を継承しているケースもあるため、
              //    「UnityEditor を最優先」にしています。
              if (isUnityEditorType)
              {
                  // UVN12: UnityEditor 型は _camelCase
                  if (!IsUnderscoreCamelCase(name))
                  {
                      Report(context, UnityEditorPrivateUnderscoreRule, field);
                  }
                  return;
              }
      
              if (isUnityRuntimeType)
              {
                  // UVN10: UnityEngine.Object 派生は camelCase(_禁止)
                  if (name.StartsWith("_", StringComparison.Ordinal) || !IsCamelCase(name))
                  {
                      Report(context, UnityPrivateCamelRule, field);
                  }
                  return;
              }
      
              // UVN11: それ以外は _camelCase
              if (!IsUnderscoreCamelCase(name))
              {
                  Report(context, NonUnityPrivateUnderscoreRule, field);
              }
          }
      
          // ---------------------------------------------------------------------
          // 4) UnityEditor 型判定
          // ---------------------------------------------------------------------
          // 「using UnityEditor; が書かれているか」ではなく、
          // 型そのものが UnityEditor アセンブリ/名前空間に属するかを判定するのが安全です。
      
          /// <summary>
          /// 型が UnityEditor 系かどうかを判定する。
          ///
          /// 優先順位:
          /// 1) もっとも堅い方法: アセンブリ名が "UnityEditor"
          /// 2) 補助方法: 名前空間が "UnityEditor" で始まる(変則ケースの保険)
          /// </summary>
          private static bool IsUnityEditorType(ITypeSymbol type)
          {
              var asmName = type.ContainingAssembly?.Name;
              if (string.Equals(asmName, "UnityEditor", StringComparison.Ordinal))
                  return true;
      
              var ns = type.ContainingNamespace?.ToDisplayString();
              if (!string.IsNullOrEmpty(ns) && ns.StartsWith("UnityEditor", StringComparison.Ordinal))
                  return true;
      
              return false;
          }
      
          // ---------------------------------------------------------------------
          // 5) 継承判定(UnityEngine.Object 派生判定に使用)
          // ---------------------------------------------------------------------
      
          /// <summary>
          /// type が baseType を継承しているか(同一型を含む)を判定する。
          /// UnityEngine.Object 派生かどうかの判定に使う。
          /// </summary>
          private static bool InheritsFrom(ITypeSymbol type, ITypeSymbol baseType)
          {
              var t = type;
              while (t != null)
              {
                  if (SymbolEqualityComparer.Default.Equals(t, baseType))
                      return true;
      
                  t = t.BaseType;
              }
              return false;
          }
      
          // ---------------------------------------------------------------------
          // 6) 命名チェック(文字列)
          // ---------------------------------------------------------------------
          // ここは「命名の形」をチェックするだけの関数。
          // 実務でルールを変える場合も、この付近を触るのが安全です。
      
          /// <summary>
          /// camelCase 判定(先頭が小文字であることだけをチェック)。
          /// 例: "player" => true, "Player" => false
          /// </summary>
          private static bool IsCamelCase(string s)
              => !string.IsNullOrEmpty(s) && char.IsLower(s[0]);
      
          /// <summary>
          /// _camelCase 判定(先頭が '_' かつ、その次が小文字)。
          /// 例: "_hp" => true, "hp" => false, "_" => false, "_Hp" => false
          /// </summary>
          private static bool IsUnderscoreCamelCase(string s)
          {
              if (string.IsNullOrEmpty(s)) return false;
              if (!s.StartsWith("_", StringComparison.Ordinal)) return false;
              if (s.Length == 1) return false;      // "_" だけはNG
              return char.IsLower(s[1]);            // "_x" で始まること
          }
      
          // ---------------------------------------------------------------------
          // 7) 診断報告(Report)
          // ---------------------------------------------------------------------
          // ここで「どの位置にエラーを出すか」を決めて ReportDiagnostic します。
      
          /// <summary>
          /// 指定したルール(DiagnosticDescriptor)で診断を報告する。
          /// 報告位置はフィールド宣言の Location を使用する。
          /// </summary>
          private static void Report(SymbolAnalysisContext context, DiagnosticDescriptor rule, IFieldSymbol field)
          {
              var loc = field.Locations.Length > 0 ? field.Locations[0] : Location.None;
              context.ReportDiagnostic(Diagnostic.Create(rule, loc, field.Name));
          }
      }
      
    • ビルド(ここでDLLができる)

      dotnet build -c Release
      

      成功すると DLL はここにできます:

      ...\UnityNamingAnalyzers\UnityNamingAnalyzers\bin\Release\netstandard2.0\UnityNamingAnalyzers.dll
      
    • Unityに組み込む(Assets/Scriptsだけに適用)

      • Unity側に配置
        Unityプロジェクトで:Assets/Scripts/Analyzers/ に UnityNamingAnalyzers.dll をコピー。配置します。
      • UnityでDLLをAnalyzerとして有効化
        Unity Editorで UnityNamingAnalyzers.dll をクリックし、Inspectorで:
        Any Platform を OFF
        その他プラットフォームも全部 OFF(入ってしまうなら)
        Asset Labels に RoslynAnalyzer を追加(完全一致)
        これで Unity のコンパイル時に走ります。
    • (注意)下記、赤枠のペン画像(編集ボタン)から "RoslynAnalyzer" と大文字小文字が完全一致した状態で入力してください。完全一致しないと正しく動きません
      大文字小文字完全に一致.png

    • 動作確認用スクリプト(Assets/Scripts配下に置く)
      Assets/Scripts/NamingTest.cs を作って貼り付け:

      using UnityEngine;
      
      #if UNITY_EDITOR
      using UnityEditor;
      #endif
      
      public class NamingTest : MonoBehaviour
      {
          // =========================
          // UVN10: UnityEngine.Object 派生 private field => camelCase(_禁止)
          // =========================
      
          private GameObject player;          // ✅ OK(Unity型 → camelCase)
          private Transform target;           // ✅ OK(Unity型 → camelCase)
      
          private GameObject _enemy;          // ❌ NG(UVN10)
          private Transform _target;          // ❌ NG(UVN10)
      
          // =========================
          // UVN11: 非Unity/非Editor private field => _camelCase
          // =========================
      
          private int _hp;                    // ✅ OK(非Unity → _camelCase)
          private string _playerName;         // ✅ OK(非Unity → _camelCase)
      
          private int hp;                     // ❌ NG(UVN11)
          private string playerName;          // ❌ NG(UVN11)
      
          // public は editorconfig 側の命名規約テスト
          public int Hp;                      // ✅ OK(public PascalCase)
      
          public void DoSomething(int argValue) // ✅ OK(method Pascal, param camel)
          {
              int localValue = 0;               // ✅ OK(local camel)
              Debug.Log(localValue + argValue);
          }
      
      #if UNITY_EDITOR
          // =========================
          // UVN12: UnityEditor 型 private field => _camelCase
          // =========================
      
          private SerializedObject _so;       // ✅ OK(UnityEditor型 → _camelCase)
          private SerializedProperty _prop;   // ✅ OK(UnityEditor型 → _camelCase)
      
          private SerializedObject so;        // ❌ NG(UVN12)
          private SerializedProperty prop;    // ❌ NG(UVN12)
      #endif
      }
      

      Unity が再コンパイルした瞬間に Console に
      ・UVN10: UnityEngine.Object派生のエラー
      ・UVN11: 非Unity/非Editor C#に関係するエラー
      ・UVN12: UnityEditor 型 に関するエラー
      それでも表示されなかったら、Assetsを選択して右クリックし Reimport All を押してプロジェクトごと再ロードしてみてください。

image.png

不足部分

  • Roslynの1つ1つの処理に関してもう少し詳しく記述
    (AIに手伝ってもらいながら突貫で作ったので手が回らず)
  • .editconfigに関しても、各設定の意味と他にどんな事ができるのか?

展望

  • 実際のプロジェクトで使われている事例が欲しいですね、効果があるのか知りたい。
  • タイポもハンドリングできるようになるとうれしい
  • Roslyn・.editconfig を使ってもっと楽ができないかを調べる。
10
10
0

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
10
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?