はじめに
前回はテンプレート展開エンジンの再帰的な展開ロジックを解説しました。今回は、生成されたコードを既存ファイルにどのようにマージするかを制御するコードマージシステム(Merger.cs と Parser.cs)を掘り下げます。
コード自動生成ツールにとって、既存の手動修正を上書きしない仕組みは非常に重要です。CodeDefinerがどのようにこの問題を解決しているか見ていきましょう。
本連載で参照するソースコードはすべて Implem/Implem.Pleasanter リポジトリのものです。リンクはコミットハッシュベースのパーマリンクを使用しています。
| 回 | テーマ |
|---|---|
| 第1回 | 全体像:エントリポイント・コマンド体系・初期化フロー |
| 第2回 | 定義ファイルとテンプレートシステム |
| 第3回 | テンプレート展開エンジン |
| 第4回(本記事) | コードマージシステム |
| 第5回 | RepeatType別のコード生成ハンドラ |
| 第6回 | プレースホルダー置換と型変換 |
マージの必要性
CodeDefinerは定義ファイルが変更されるたびにコードを再生成します。しかし、生成されたコードに開発者が手動で修正を加えることがあります。たとえば以下のような場合です。
- 自動生成されたモデルクラスに固有のビジネスロジックを追加する
- パフォーマンスチューニングのためにクエリを最適化する
- 特殊なケースに対応するために例外処理を追加する
これらの手動修正は、コードの再生成時に失われてはいけません。
Merger.Merge():マージのエントリポイント
internal static void Merge(string fileName, string code, bool margeToExisting)
{
var newCode = code;
var path = Path.Combine(Directories.ServicePath(), fileName);
if (path.Exists())
{
var existingCode = Files.Read(path);
if (path.FileExtension() == ".cs")
{
newCode = MergedCode(newCode, existingCode, fileName, margeToExisting);
}
CodeWriter.Write(path, newCode, existingCode);
}
else
{
CodeWriter.Write(path, newCode);
}
}
処理フロー
重要なポイントは以下の2つです。
- C#ファイルのみマージ処理が行われる(.cs 以外は単純上書き)
-
CodeWriter.Write()は既存コードと差分がない場合は書き込みをスキップする
MergeToExisting フラグ
MergeToExisting は CodeDefinition のプロパティで、マージの方向を決定します。
private static string MergedCode(
string newCode, string existingCode, string fileName, bool margeToExisting)
{
var newCsParser = new Parser(newCode);
var existingCsParser = new Parser(existingCode);
if (margeToExisting)
{
// 既存コードに新コードをマージ
existingCsParser.MergeCode(newCsParser, margeToExisting);
return existingCsParser.Code();
}
else
{
// 新コードに既存コードをマージ
newCsParser.MergeCode(existingCsParser, margeToExisting);
return newCsParser.Code();
}
}
| MergeToExisting | ベースとなるコード | マージされるコード | ユースケース |
|---|---|---|---|
true |
既存コード | 新しい生成コード | 既存の手動修正を保持しつつ新機能を追加 |
false |
新しい生成コード | 既存コード | 生成コードを優先しつつ手動修正を取り込み |
Parser クラス:C#コードの構造解析
マージ処理の根幹を担うのが Parser クラスです。C#ソースコードを名前空間→クラス→メソッドの階層構造に分解します。
internal class Parser
{
private string code;
internal CodeTypes CodeType; // Namespace / Class / Method
internal string Id; // メンバーのシグネチャ
internal string Name; // メンバー名
internal bool Fixed; // 手動修正保護マーカー
internal bool NotMerge; // マージ除外マーカー
internal string Description; // XMLドキュメントコメント
internal int IndexBase; // 開始位置
internal int Indent; // インデント深度
internal Dictionary<int, string> TextCollection; // テキスト片
internal Dictionary<int, Parser> MemberCollection; // 子メンバー
internal enum CodeTypes : int
{
Namespace,
Class,
Method
}
}
構造解析の流れ
コード構造の分解例
以下のC#コードがどのように分解されるか見てみましょう。
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
/// Fixed:
public string CustomMethod()
{
return "手動で追加したメソッド";
}
public void GeneratedMethod()
{
// 自動生成されたメソッド
}
}
}
Fixed マーカーと NotMerge マーカー
コードマージの振る舞いを制御する2つの特殊なマーカーがあります。
/// Fixed:
/// Fixed:
public string CustomMethod()
{
return "この内容は再生成時に保持される";
}
/// Fixed: マーカーが付いたメンバーは、コード再生成時に既存の内容が保持されます。マージ処理では、このメンバーの中身を新しい生成コードで上書きせず、既存の実装を維持します。
/// NotMerge:
/// NotMerge:
public void SpecialMethod()
{
// マージ処理自体から除外される
}
/// NotMerge: マーカーが付いたメンバーは、マージ処理から完全に除外されます。
マーカーの検出
private void SetFixed()
{
Fixed = Description.IndexOf("/// Fixed:") != -1;
}
private void SetNotMerge()
{
NotMerge = Description
.RegexExists("^ {" + Indent + "}/// NotMerge:.*", RegexOptions.Multiline);
}
MergeCode():マージの核心ロジック
MergeCode() メソッドは、2つのパース済みコード構造を比較し、マージを行います。
internal void MergeCode(Parser margeCs, bool margeToExisting)
{
MergeCode(null, this, margeCs, margeToExisting);
}
private void MergeCode(
Parser baseCsParent, Parser baseCs, Parser margeCs, bool margeToExisting)
{
// ステップ1: Fixed メンバーの処理
if (margeCs.Fixed)
{
MergeFixedCode(baseCsParent, baseCs, margeCs);
}
// ステップ2: MergeToExisting フラグに基づくマージ
if (margeToExisting)
{
MergeToExisting(baseCsParent, baseCs, margeCs);
}
// ステップ3: 子メンバーに対して再帰的にマージ
margeCs.MemberCollection.Select(o => o.Value).ForEach(memberOld =>
MergeCode(baseCs, SameMember(baseCs, memberOld), memberOld, margeToExisting));
}
マージの判定ロジック
SameMember:同名メンバーの検索
private Parser SameMember(Parser baseCs, Parser margeCs)
{
return baseCs != null
? baseCs.MemberCollection
.Select(o => o.Value)
.FirstOrDefault(o => o.Id == margeCs.Id)
: null;
}
メンバーの同一性は Id(メソッドシグネチャ全体)で判定されます。メソッド名だけでなく、アクセス修飾子や引数リストも含めたシグネチャで比較するため、オーバーロードも正しく区別されます。
CodeWriter:差分がある場合のみ書き込み
マージ処理の最終段階で CodeWriter.Write() が呼ばれます。
internal static void Write(string codePath, string newCode, string existingCode = "")
{
if (existingCode.IsNullOrEmpty() || newCode != existingCode)
{
Consoles.Write(
codePath.Substring(Directories.ServicePath().Length),
Consoles.Types.Info);
newCode.Write(codePath);
}
else
{
Consoles.Write("-", Consoles.Types.Info);
}
}
- 既存コードと新コードが完全に同じ場合、ファイルへの書き込みはスキップされ、コンソールに
"-"が表示される - 差分がある場合のみファイルに書き込まれ、変更されたファイルパスがコンソールに表示される
これにより、不必要なファイル変更(タイムスタンプの更新やGitの差分ノイズ)を防ぎます。
マージの具体例
シナリオ:既存ファイルにFixedメソッドがある場合
既存ファイル(生成済み + 手動修正あり):
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
public string LoginId { get; set; }
/// Fixed:
public bool ValidateCustomRule()
{
// 開発者が手動で追加したバリデーション
return LoginId.Length >= 3;
}
public void Update()
{
// 自動生成された更新処理
}
}
}
新しい生成コード(定義変更後):
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
public string LoginId { get; set; }
public string Name { get; set; } // 新しいカラムが追加された
public void Update()
{
// 自動生成された更新処理(変更あり)
}
}
}
マージ結果(MergeToExisting = false の場合):
namespace Implem.Pleasanter.Models
{
public class UserModel : _BaseModel
{
public int UserId { get; set; }
public string LoginId { get; set; }
public string Name { get; set; } // 新カラムが反映
/// Fixed:
public bool ValidateCustomRule()
{
// 開発者が手動で追加したバリデーション(保持される!)
return LoginId.Length >= 3;
}
public void Update()
{
// 自動生成された更新処理(最新版に更新)
}
}
}
-
Nameプロパティが新コードから追加された -
ValidateCustomRule()は/// Fixed:マーカーにより既存の実装が保持された -
Update()メソッドは新しい生成コードで更新された
Code():パース結果からコードを再構築
マージ後のパース結果を文字列に戻す Code() メソッドも重要です。
internal string Code()
{
if (TextCollection.Count == 0)
{
// テキストがない場合はメンバーだけを結合
return MemberCollection
.Take(1)
.ToDictionary(o => o.Key, o => o.Value.Code())
.Union(MemberCollection.Skip(1)
.ToDictionary(o => o.Key, o => "\r\n" + o.Value.Code()))
.OrderBy(o => o.Key)
.Select(o => o.Value)
.Join(string.Empty);
}
else
{
// テキストとメンバーをインデックス順に結合
return TextCollection
.ToDictionary(o => o.Key, o => CrlfAdjustedCode(o.Key, o.Value))
.Union(MemberCollection
.ToDictionary(o => o.Key, o => o.Value.Code()))
.OrderBy(o => o.Key)
.Select(o => o.Value)
.Join(string.Empty);
}
}
TextCollection と MemberCollection をインデックス順に並べ替えてから結合することで、コード内のメンバーの出現順序を維持します。
まとめ
第4回では、CodeDefinerのコードマージシステムを解説しました。
-
Merger.Merge()はC#ファイルのみマージ処理を行い、それ以外は単純上書きする -
MergeToExistingフラグでマージの方向(ベースとなるコード)を制御する -
ParserクラスがC#コードを 名前空間→クラス→メソッド の階層構造に分解する -
/// Fixed:マーカーで手動修正を保護し、再生成時に上書きされない -
/// NotMerge:マーカーでマージ処理から完全に除外する -
CodeWriterは差分がある場合のみファイルに書き込む
次回は、RepeatType 別のコード生成ハンドラ(Table、Column、Join など)の詳細を解説します。