detect-static-dependencies について
テストしにくい静的・環境依存を C#/.NET コードベースから調べるスキルです。呼び出し箇所や影響ファイルを順位付けし、テスト容易性の改善に使える既存の抽象化を示します。
主な機能
- 01C# ファイルの依存パターン検索
- 02呼び出し箇所の集計と順位付け
- 03影響ファイルと位置のレポート
- 04.NET 抽象化の候補提示
活用シーン
- 01ユニットテスト前のテスト容易性評価
- 02レガシーコードの静的結合の把握
- 03ラッパー移行の優先順位付け
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dotnet/ skills --skill detect-static-dependenciesインストール後、Claude Code で/detect-static-dependenciesを入力して読み込まれたか確認します。
詳細
静的依存関係を検出する
C# のコードベースをスキャンして、テストが難しい静的 API の呼び出しを検出し、最も頻繁に使われている静的 API、影響を受けるファイル、置き換えに利用できる .NET エコシステム内の既存の抽象化を示す、順位付けされたレポートを作成します。
使用する場面
- 単体テストを追加する前に、プロジェクトのテスト容易性を監査する
- レガシーコードベースにおける静的依存の範囲を把握する
- 最初にラップする静的 API の優先順位を付ける(使用頻度の高いものを優先)
- テスト容易性を段階的に改善するための移行計画を作成する
回答ガイドライン
- 回答の範囲はユーザーの依頼に合わせてください。特定のカテゴリ(例:「時間に関する静的 API を探す」)についての質問には、そのカテゴリに絞り、ファイルの場所と件数を示してください。すべてのカテゴリを対象とする完全なレポートは作成しないでください。
- ユーザーが特定のファイルまたはディレクトリのパスを指定した場合は、その範囲のみをスキャンしてください。依頼がない限り、ソリューション全体に対象を広げないでください。
- ステップ 4 の完全な構造化レポート形式は、包括的な監査の依頼に対して使用します。対象を絞った質問には、関連する部分のみ(例:依頼されたカテゴリの概要と該当ファイル)を返してください。
実行条件
- プロンプトに相対パスが記載されていれば、作業を開始できます。利用可能なファイル一覧ツールでそのパスを特定し、すぐにスキャンしてください。発見と内容検索の両方に失敗するまでは、ユーザーにファイルの提供や再アップロードを求めないでください。
- 対象となる
.csファイルを、行番号付きの再帰的な内容検索でスキャンしてください。staticキーワードだけを検索しないでください。LINQ 式、ラムダ、コールバック、補間文字列内のアンビエント呼び出しには、通常static修飾子がありません。 - 一覧表示または検索によって存在が確認されたパスでファイル読み込みツールが失敗した場合は、再試行する前に失敗の種類を判定してください。正規パスが引き続きワークスペース内にあることを確認したうえで、ツールの利用可否、転送、またはパスの正規化に関する失敗だと確認できた場合に限り、
rg -n、grep、シェルのファイルリーダーなど、利用可能な別の方法に切り替えてください。内容の除外、権限またはポリシー、ワークスペース境界、原因不明の失敗が発生した場合は停止してください。検索結果は出現箇所の記録に使用できます。レシーバーの出所を確認するために必要な周辺コードのみを開いてください。 - このスキルを読み込んだり、スキャン計画を告げたりしただけで停止しないでください。同じ応答内で監査結果を返してください。すべての代替手段が実際に失敗した場合は、確認済みの部分的な調査結果と具体的な制約を報告してください。調査結果を捏造したり、再実行の依頼で監査結果を置き換えたりしないでください。
使用しない場面
- ユーザーがラッパーの生成を希望している(
generate-testability-wrappersに引き継ぐ) - ユーザーが機械的な移行の実行を希望している(
migrate-static-to-wrapperに引き継ぐ) - 静的 API がすでにインターフェイスまたは
TimeProviderの背後にある - コードが C# / .NET ではない
入力
| 入力 | 必須 | 説明 |
|---|---|---|
| 対象パス | いいえ | スキャン対象のファイル、ディレクトリ、プロジェクト(.csproj)、またはソリューション(.sln)。既定では現在のワークスペースです。 |
| 除外パターン | いいえ | スキップする Glob パターン(例:**/obj/**、**/Migrations/**) |
| カテゴリフィルター | いいえ | 対象を特定のカテゴリに限定します:time、filesystem、environment、network、console、process |
ワークフロー
ステップ 1:スキャン範囲を決定する
対象を .cs ファイルの集合に解決します。
- プロンプトに記載されたワークスペース相対パスを対象として扱い、ユーザーに絶対パスを求めるのではなく、そのパスを特定してください。
- 省略されている場合は、現在のワークスペース内にある対象の
.csファイルをすべてスキャンしてください。特定のプロジェクトだけを選び、他のプロジェクトを黙って除外しないでください。 .csファイルの場合は、そのファイルのみをスキャンしてください。- ディレクトリの場合は、再帰的にすべての
.csファイルをスキャンしてください(obj/、bin/は除外)。 .csprojの場合は、そのディレクトリを特定し、配下の.csファイルをスキャンしてください。.slnの場合は、解析してすべてのプロジェクトディレクトリを特定し、全プロジェクトの.csファイルをスキャンしてください。
常に obj/、bin/ と、ユーザー指定の除外パターンを除外してください。
ステップ 2:静的依存関係のパターンを検索する
各ファイルをスキャンし、次のカテゴリに該当する呼び出しを探します。
パターンに一致したものは候補として扱い、問題として確定する前に、呼び出し元のクラスにレシーバーがどのように渡されるかを追跡します。コンストラクター、パラメーター、プロパティ、または依存性注入(DI)によって提供されるコラボレーターは、すでにテスト用の差し替え口になっています。特に、注入された HttpClient は制御可能な HttpMessageHandler を使ってテストできます。注入された型が具象型であるというだけで、その呼び出しを数えたり、置き換えを推奨したりしないでください。
| カテゴリ | 検索するパターン | 推奨される置き換え |
|---|---|---|
| 時間 | DateTime.Now、DateTime.UtcNow、DateTime.Today、DateTimeOffset.Now、DateTimeOffset.UtcNow、Task.Delay(、new CancellationTokenSource(TimeSpan | TimeProvider(.NET 8 以降) |
| ファイルシステム | File.ReadAllText(、File.WriteAllText(、File.Exists(、File.Delete(、File.Copy(、File.Move(、Directory.Exists(、Directory.CreateDirectory(、Directory.GetFiles(、Directory.Delete(、Path.GetTempPath(、およびディスクにアクセスするインスタンスメンバー(new FileInfo(...)、new DirectoryInfo(...)、.LastWriteTimeUtc、new StreamReader(path)) | IFileSystem(System.IO.Abstractions NuGet) |
| 乱数/識別情報 | new Random(、Random.Shared、Guid.NewGuid( | TimeProvider 形式の差し替え口:Random/IGuidProvider を注入 |
| カルチャ/シリアル化 | CultureInfo.CurrentCulture、CultureInfo.CurrentUICulture、JsonSerializer.Serialize(、JsonSerializer.Deserialize( | カルチャ/オプションを明示的に渡すか、シリアライザー抽象化を注入 |
| 環境 | Environment.GetEnvironmentVariable(、Environment.SetEnvironmentVariable(、Environment.MachineName、Environment.UserName、Environment.CurrentDirectory、Environment.Exit( | カスタムの IEnvironmentProvider |
| ネットワーク | new HttpClient(、.GetAsync(、.PostAsync(、.SendAsync((レシーバーが HttpClient であることを確認し、レシーバーが注入されたもの、または注入されたファクトリーが生成したものによる呼び出しは除外) | HttpClient を注入(通常は IHttpClientFactory から提供) |
| コンソール | Console.WriteLine(、Console.ReadLine(、Console.Write(、Console.ReadKey( | IConsole ラッパーまたは ILogger |
| プロセス | Process.Start(、Process.GetCurrentProcess(、Process.GetProcessesByName( | カスタムの IProcessRunner |
時間に関する呼び出しは、数だけでなく使われ方も確認します。1 つの論理操作内にある環境依存の時計の読み取りが 2 つなら、それぞれが呼び出し箇所であり、一貫性の不具合です。たとえば、CreatedAt と ExpiresAt = DateTime.UtcNow.AddDays(30) に対して DateTime.UtcNow を別々に読み取ると、時刻にずれが生じる可能性があります。1 つの時点を取得して使うことを推奨してください。TimeProvider を使う場合は、可能な限り DateTimeOffset を維持します。既存のメンバーが UTC の DateTime を必要とする場合は、UTC の種類情報が失われる .DateTime ではなく、GetUtcNow().UtcDateTime を使います。1 つの時点を取得することは、動作レベルでの任意の追加改善として扱ってください。機械的なラッパーへの移行では、ユーザーがその意味上の変更を別途承認しない限り、元の読み取りを 1 対 1 で維持する必要があります。
Step 3: 結果を集計して順位付けする
スキャン対象範囲全体で各呼び出し箇所を数えます。以下のルールで対象とするインスタンスメンバーの呼び出し箇所も含め、static の呼び出し箇所だけに限定しないでください。
カウントルール — このレポートの精度が場当たり的なスキャンに劣る主な原因は、合計の誤りです。
- 説明を書く前に、出現箇所台帳を作成します。 対象に含める各呼び出し箇所について、カテゴリ、正確なパターン、
file:line、推奨するシームを記載した行を1つだけ作成します。すべてのカテゴリ、パターン、ファイルごとの件数は、同じ台帳をグループ化して算出し、表を作成するときに個別に再集計しないでください。 - 3つのカウント範囲を分けて扱います。
Files scannedは対象となるすべてのソースファイルを含み、affected filesは台帳に行があるファイルのみを含み、call sitesは台帳の行数です。これらを混同しないでください。 - 合計の出典は1つに統一します。 見つかったすべての呼び出し箇所をカテゴリ別の概要と総計に含めます。実際の検出結果を「additional observations」セクションに分け、合計から除外しないでください。
- メンバーが
staticかどうかではなく、何にアクセスするかで分類します。 同じテスト不能なリソースにアクセスするインスタンスメンバーもカウントし、該当するカテゴリに含めます(new FileInfo(path).LastWriteTimeUtc→ File System、new HttpClient().GetAsync(...)→ Network)。メンバーがインスタンス呼び出しの場合は、「static」ではなく「隠れた依存関係」と記載してください。 - インスタンス呼び出しをカウントする前に、レシーバーの由来を確認します。 テスト対象のコード自身が依存関係を取得または生成している場合に限り、リソースへのアクセスをカウントします。コンストラクター、パラメーター、プロパティ、DI から注入される協調オブジェクトは、「ラップが必要」な合計から除外してください。具象
HttpClientインスタンスも同様です。 - 決定的な純粋ヘルパーは「ラップが必要」な合計から除外します。
Path.Combine、Path.GetExtension、Path.GetFileName、Math.*/string.*の静的メソッドは、環境依存の入力を受け取らず、容易にテストできます。記載する場合は、別の「対応不要」注記に記載し、テスト容易性を妨げる要因として扱わないでください。 - 報告前にすべてのカテゴリを確認します — 時刻、ファイルシステム、環境、ネットワーク、コンソール、プロセス、乱数(
new Random()、Guid.NewGuid())、カルチャ(CultureInfo.CurrentCulture)、シリアル化/JsonSerializerなどの静的メソッド。該当するカテゴリを省略すると、カウントが過少になります。 - 各出現箇所に
file:lineを記載します。 ユーザーがすぐに該当箇所へ移動できるようにしてください。 - 公開前に整合性を確認します。 カテゴリ別の合計、上位パターンの表、ファイルごとの表の合計は、同じ総計になる必要があります。
- 除外対象はカテゴリではなく、スコープの決定事項として扱います。 台帳を作成する前に、
obj/、bin/、生成ファイル、ユーザーが除外したファイルを対象から外します。これらのファイルや呼び出し箇所を、報告するカウントに含めないでください。除外については一度だけ記載し、集計に除外候補を混在させないでください。 - 順位を絞り込んだ場合は、その旨を明記します。 包括的な監査では、整合性の確認に必要な場合、異なるパターンをすべて記載します。ユーザーが上位 N 件のみを求めた場合は、それが一部であることを明記し、行数の合計が総計になるかのように示さないでください。
以下を含む概要を作成します。
- カテゴリ別の概要 — カテゴリごとの呼び出し箇所の合計(時刻、ファイルシステム、環境など)
- 上位パターン — 頻度の高い個別パターンを上位10件まで、件数順に表示
- 影響の大きいファイル — 静的依存関係が最も多いファイル
- 利用可能な既存の抽象化 — 各カテゴリについて、推奨する .NET の抽象化を記載します。
- Time →
TimeProvider(.NET 8 以降に標準搭載) - File system →
System.IO.Abstractions(NuGet パッケージ) - HTTP →
IHttpClientFactory(標準搭載) - Environment → カスタム
IEnvironmentProvider - Console → カスタム
IConsoleまたはILogger - Process → カスタム
IProcessRunner
- Time →
Step 4: レポートを提示する
次の形式で、構造化されたレポートを作成します。
## Static Dependency Report
**Scope**: <project/solution name>
**Files scanned**: <count>
**Total static call sites**: <count>
### Category Summary
| Category | Call Sites | Recommended Abstraction |
|-------------|-----------|------------------------|
| Time | 42 | TimeProvider (.NET 8+) |
| File System | 31 | System.IO.Abstractions |
| Environment | 12 | IEnvironmentProvider |
| ... | ... | ... |
### Top 10 Patterns
| # | Pattern | Count | Files |
|---|---------------------|-------|-------|
| 1 | DateTime.UtcNow | 28 | 14 |
| 2 | File.ReadAllText | 18 | 9 |
| ... |
### Most Affected Files
| File | Static Calls | Categories |
|-------------------------------|-------------|---------------------|
| Services/OrderProcessor.cs | 12 | Time, FileSystem |
| ... |
### Migration Priority
1. **Time** (42 sites) — Use `TimeProvider`, zero NuGet dependencies on .NET 8+
2. **File System** (31 sites) — Use `System.IO.Abstractions` NuGet package
3. ...
Step 5: 次のステップを提案する
レポートに基づき、最初に取り組むべきカテゴリー(該当数が最も多く、組み込みサポートが最も充実しているもの)を提案します。数行にまとめてください。
ユーザーの次のアクションで明らかに必要な場合に限り、generate-testability-wrappers または migrate-static-to-wrapper に触れてください。これは引き継ぎのメモであり、宣伝文句ではありません。調査結果をぼかすような宣伝的な次のステップで監査を終えてはいけません。
検証
- 対象範囲内のすべての
.csファイルをスキャンした(件数を確認) - レポートにカテゴリーごとの合計、上位のパターン、該当ファイルが含まれている
- カテゴリーごとの合計、上位のパターン、ファイルごとの件数が同じ総計に一致している
- スキャンしたファイル数、該当ファイル数、呼び出し箇所数をそれぞれ別の数量として報告している
- すべての集計を、個別に数え直すのではなく、1つの発生箇所台帳から算出している
- すべての発生箇所に
file:lineの位置情報がある - 「追加」セクションに、合計に含まれない検出事項を残していない
- 注入された協調オブジェクトへの呼び出しを「ラップが必要」の合計から除外している
- 決定論的な純粋ヘルパー(
Path.Combine、Math.*)をテスト容易性の妨げとして数えていない - 検出された各パターンに、推奨する置き換え方法を記載している
-
obj/とbin/ディレクトリを除外している - 移行の優先順位を、影響度(件数 × 置き換えやすさ)に基づいて並べている
よくある落とし穴
| 落とし穴 | 解決策 |
|---|---|
obj/ や生成コードをスキャンする | 常に obj/、bin/、*.Designer.cs を除外します |
| 注入された協調オブジェクトへの呼び出しを数える | レシーバーをたどって確認します。注入された HttpClient、TimeProvider、インターフェース、その他の呼び出し元から渡される依存関係には、すでに差し替え可能な接点があるため、置き換えは不要です |
| ラムダ/LINQ 内の静的メンバーを見落とす | 検索対象には、ラムダを含む .cs ファイル内のすべてのコードが含まれます |
.NET 8 より前で TimeProvider を推奨する | .csproj の TargetFramework を確認します。net8.0 未満の場合は、NodaTime.IClock またはカスタムの ISystemClock を推奨します |
| テストプロジェクトを無視する | 本番コードのみをスキャンします。スキャン対象から *.Tests.csproj プロジェクトを除外します |
| 検出事項を別枠にして過少計上する | 実際の呼び出し箇所はカテゴリーごとの合計に含めます。合計に反映されない末尾の「ほかにも気づいたこと」という段落に分けてはいけません |
| インスタンスメンバーを静的メンバーと呼ぶ | new FileInfo(p).LastWriteTimeUtc はインスタンス呼び出しですが、隠れたファイルシステム依存関係でもあります。ファイルシステムのカテゴリーに数え、正確に説明します |
Path.Combine にラッパーを推奨する | 純粋で決定論的なヘルパーには差し替え可能な接点は不要です。妨げとして挙げると、推奨が誤ったものになります |