1.プロジェクト作成&依存追加
コマンドプロンプト.cmd
rem プロジェクト作成
mkdir VbTopCallers
cd VbTopCallers
dotnet new console -n VbTopCallers
cd VbTopCallers
rem Roslyn 関連パッケージを追加
dotnet add package Microsoft.CodeAnalysis.CSharp
dotnet add package Microsoft.CodeAnalysis.VisualBasic
dotnet add package Microsoft.CodeAnalysis.Workspaces.MSBuild
dotnet add package Microsoft.CodeAnalysis.CSharp.Workspaces
dotnet add package Microsoft.CodeAnalysis.VisualBasic.Workspaces
rem MSBuild 検出用
dotnet add package Microsoft.Build.Locator
2.Program.cs を全文置き換え(下のコードをコピペ)
Program.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.MSBuild;
// ★ ここで C# / VB 構文ノードにエイリアスを定義(曖昧さ解消)
using Cs = Microsoft.CodeAnalysis.CSharp.Syntax;
using Vb = Microsoft.CodeAnalysis.VisualBasic.Syntax;
class Program
{
static async Task<int> Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Usage: VbTopCallers <solution.sln> <Namespace.Type.Member> [maxDepth]");
return 1;
}
var slnPath = args[0];
var targetFqn = args[1];
var maxDepth = (args.Length >= 3 && int.TryParse(args[2], out var d)) ? d : 50;
try
{
if (!MSBuildLocator.IsRegistered)
{
var inst = MSBuildLocator.QueryVisualStudioInstances()
.OrderByDescending(i => i.Version).FirstOrDefault();
if (inst == null)
{
Console.WriteLine("MSBuild / Build Tools が見つかりません。");
return 2;
}
MSBuildLocator.RegisterInstance(inst);
}
using var workspace = MSBuildWorkspace.Create();
Console.WriteLine($"Opening solution: {slnPath}");
var solution = await workspace.OpenSolutionAsync(slnPath);
var target = await FindSymbolAsync(solution, targetFqn);
if (target == null)
{
Console.WriteLine($"対象メンバーが見つかりません: {targetFqn}");
return 3;
}
var completed = new List<(List<ISymbol> path, string role)>();
await ExploreUpstreamAsync(
target, solution, maxDepth,
new Stack<ISymbol>(), completed, new HashSet<string>(), "");
WriteTopCallersCsv_Dedup(completed, target, solution, "top_callers.txt");
Console.WriteLine("Done. - top_callers.txt");
return 0;
}
catch (Exception ex)
{
Console.WriteLine("Error: " + ex);
return 9;
}
}
//======================== 表示フォーマット ========================
private static readonly SymbolDisplayFormat HierarchyFormat =
new SymbolDisplayFormat(
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
memberOptions: SymbolDisplayMemberOptions.IncludeContainingType
);
private static string ShowFull(ISymbol s) => s.ToDisplayString(HierarchyFormat);
//======================== シンボル取得 =============================
private static async Task<ISymbol?> FindSymbolAsync(Solution solution, string fqn)
{
var lastDot = fqn.LastIndexOf('.');
if (lastDot < 0) return null;
var typeFqn = fqn.Substring(0, lastDot);
var memberName = fqn.Substring(lastDot + 1);
foreach (var project in solution.Projects)
{
var compilation = await project.GetCompilationAsync();
if (compilation == null) continue;
var type = compilation.GetTypeByMetadataName(typeFqn);
// VB の RootNamespace 補完(例: COSCSS.DAC.DEPT)
if (type == null && compilation.Language == LanguageNames.VisualBasic)
{
var rootNs = compilation.AssemblyName;
if (!string.IsNullOrEmpty(rootNs))
{
var vbTypeFqn = $"{rootNs}.{typeFqn}";
type = compilation.GetTypeByMetadataName(vbTypeFqn);
}
}
if (type != null)
{
var member = type.GetMembers()
.FirstOrDefault(m => m.Name == memberName &&
(m is IMethodSymbol or IPropertySymbol or IFieldSymbol or IEventSymbol));
if (member != null) return member;
}
}
return null;
}
//======================== 上流探索 ================================
private static async Task ExploreUpstreamAsync(
ISymbol callee,
Solution solution,
int depth,
Stack<ISymbol> current,
List<(List<ISymbol> path, string role)> completed,
HashSet<string> pathVisited,
string role)
{
if (depth <= 0)
{
var path0 = current.Reverse().ToList();
path0.Add(callee);
completed.Add((path0, role));
return;
}
var refs = await SymbolFinder.FindReferencesAsync(callee, solution);
var callerSyms = new List<(IMethodSymbol sym, string role)>();
// メソッドの場合は多態性(override/virtual)対策として実際の解決先を照合
IMethodSymbol? targetMethod = callee as IMethodSymbol;
foreach (var r in refs)
{
foreach (var loc in r.Locations)
{
var tree = loc.Location.SourceTree;
if (tree == null) continue;
var doc = solution.GetDocument(tree);
if (doc == null) continue;
var model = await doc.GetSemanticModelAsync();
if (model == null) continue;
var root = await tree.GetRootAsync();
var node = root.FindNode(loc.Location.SourceSpan);
if (node == null) continue;
// --- 実際の解決先メソッドを確認(多態性対策) ---
if (targetMethod != null)
{
IMethodSymbol? resolved = null;
// C#
var invCs = node.FirstAncestorOrSelf<Cs.InvocationExpressionSyntax>();
if (invCs != null)
resolved = model.GetSymbolInfo(invCs).Symbol as IMethodSymbol;
// VB
if (resolved == null)
{
var invVb = node.FirstAncestorOrSelf<Vb.InvocationExpressionSyntax>();
if (invVb != null)
resolved = model.GetSymbolInfo(invVb).Symbol as IMethodSymbol;
}
if (resolved != null && !IsSameOrOverride(resolved, targetMethod))
continue; // 別実装に解決された参照は捨てる
}
var roleStr = DetermineRefRole(node, doc.Project.Language);
var parentMethod = model.GetEnclosingSymbol(loc.Location.SourceSpan.Start) as IMethodSymbol;
if (parentMethod != null && IsFromSource(parentMethod) && IsInteresting(parentMethod))
callerSyms.Add((parentMethod, roleStr));
}
}
if (callerSyms.Count == 0)
{
var path1 = current.Reverse().ToList();
path1.Add(callee);
completed.Add((path1, role));
return;
}
foreach (var (caller, refRole) in DistinctBy(callerSyms, x => Key(x.sym)))
{
var key = Key(caller);
if (pathVisited.Contains(key)) continue;
pathVisited.Add(key);
current.Push(caller);
await ExploreUpstreamAsync(caller, solution, depth - 1, current, completed, pathVisited, refRole);
current.Pop();
pathVisited.Remove(key);
}
}
// .NET 標準互換:DistinctBy 互換実装(.NET 6 未満でもOK)
private static IEnumerable<T> DistinctBy<T, TKey>(IEnumerable<T> src, Func<T, TKey> keySelector)
{
var seen = new HashSet<TKey>();
foreach (var x in src)
if (seen.Add(keySelector(x))) yield return x;
}
// override/基底の連鎖で同一とみなす
private static bool IsSameOrOverride(IMethodSymbol found, IMethodSymbol target)
{
if (SymbolEqualityComparer.Default.Equals(found.OriginalDefinition, target.OriginalDefinition))
return true;
for (var m = found.OverriddenMethod; m != null; m = m.OverriddenMethod)
if (SymbolEqualityComparer.Default.Equals(m.OriginalDefinition, target.OriginalDefinition))
return true;
for (var m = target.OverriddenMethod; m != null; m = m.OverriddenMethod)
if (SymbolEqualityComparer.Default.Equals(found.OriginalDefinition, m.OriginalDefinition))
return true;
return false;
}
// 参照方向(RefRole)判定:左辺=AssignedTarget / 右辺=ReferenceSource / その他=Reference
private static string DetermineRefRole(SyntaxNode node, string language)
{
try
{
if (language == LanguageNames.CSharp && node.Parent is Cs.AssignmentExpressionSyntax assign)
{
if (assign.Left == node) return "AssignedTarget";
if (assign.Right == node) return "ReferenceSource";
}
else if (language == LanguageNames.VisualBasic && node.Parent is Vb.AssignmentStatementSyntax vbAssign)
{
if (vbAssign.Left == node) return "AssignedTarget";
if (vbAssign.Right == node) return "ReferenceSource";
}
}
catch { }
return "Reference";
}
private static bool IsFromSource(IMethodSymbol m)
=> m.Locations.Any(l => l.IsInSource);
private static bool IsInteresting(IMethodSymbol m)
{
if (m.MethodKind == MethodKind.PropertyGet || m.MethodKind == MethodKind.PropertySet) return false;
if (m.Name.StartsWith("<")) return false; // 生成メソッドなど
return true;
}
//======================== CSV 出力 ================================
private static void WriteTopCallersCsv_Dedup(
List<(List<ISymbol> path, string role)> completed,
ISymbol target,
Solution solution,
string file)
{
// 同じ最上流(1つ上の呼び出し)に対して最短経路を1本だけ残す
var byTop = new Dictionary<string, (List<ISymbol> path, string role)>(StringComparer.Ordinal);
foreach (var (path, role) in completed)
{
if (path.Count == 0) continue;
var topMost = path.Count >= 2 ? path[path.Count - 2] : path.First();
var topKey = Key(topMost);
if (!byTop.TryGetValue(topKey, out var existing) || path.Count < existing.path.Count)
byTop[topKey] = (path, role);
}
using var sw = new StreamWriter(file, false);
sw.WriteLine("Namespace,File,Call,LineStart,IsEntryPoint,CallHierarchy,RefRole");
foreach (var kv in byTop.OrderBy(k => k.Key))
{
var (path, role) = kv.Value;
var topMost = path.Count >= 2 ? path[path.Count - 2] : path.First();
var ns = topMost.ContainingNamespace?.ToDisplayString() ?? "";
var (fullPath, start) = GetDeclSpan(topMost);
bool isEntry = topMost is IMethodSymbol ms &&
IsEntryPoint(ms, solution).GetAwaiter().GetResult();
// CallHierarchy: 改行+"<"、完全修飾名
var chainParts = new List<string> { ShowFull(target) };
for (int i = 0; i <= path.Count - 2; i++)
chainParts.Add(ShowFull(path[i]));
var chain = string.Join("\n<", chainParts);
sw.WriteLine($"{Csv(ns)},{Csv(fullPath)},{Csv(Key(topMost))},{start},{(isEntry ? "True" : "False")},{Csv(chain)},{Csv(role)}");
}
}
//======================== 付帯ユーティリティ ======================
private static async Task<bool> IsEntryPoint(IMethodSymbol m, Solution solution)
{
var declRef = m.DeclaringSyntaxReferences.FirstOrDefault();
if (declRef != null)
{
var node = declRef.GetSyntax();
if (node.Language == LanguageNames.VisualBasic)
{
if (node is Vb.MethodBlockSyntax mb && mb.SubOrFunctionStatement?.HandlesClause != null)
return true;
if (node is Vb.MethodStatementSyntax ms && ms.HandlesClause != null)
return true;
}
}
if (await IsWiredByEventAsync(m, solution)) return true;
if (string.Equals(m.Name, "Main", StringComparison.Ordinal) &&
m.IsStatic &&
(m.ReturnsVoid || m.ReturnType?.SpecialType == SpecialType.System_Int32))
return true;
if (m.IsOverride && DerivesFrom(m.ContainingType, "System.Windows.Forms.Form"))
{
var lifecycle = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ "OnLoad","OnShown","OnClosing","OnClosed","OnClick","OnPaint","OnKeyDown","OnKeyUp" };
if (lifecycle.Contains(m.Name)) return true;
}
return false;
}
// AddHandler(VB)等でイベントに結線されているか
private static async Task<bool> IsWiredByEventAsync(IMethodSymbol m, Solution solution)
{
foreach (var project in solution.Projects)
{
foreach (var doc in project.Documents)
{
var root = await doc.GetSyntaxRootAsync();
var model = await doc.GetSemanticModelAsync();
if (root == null || model == null) continue;
if (doc.Project.Language == LanguageNames.VisualBasic)
{
int addHandlerKind = (int)Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.AddHandlerStatement;
int addressOfKind = (int)Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.AddressOfExpression;
foreach (var add in root.DescendantNodes().Where(n => n.RawKind == addHandlerKind))
{
var addr = add.ChildNodes().FirstOrDefault(n => n.RawKind == addressOfKind);
if (addr != null)
{
var sym = model.GetSymbolInfo(addr).Symbol as IMethodSymbol;
if (sym != null &&
SymbolEqualityComparer.Default.Equals(sym.OriginalDefinition, m.OriginalDefinition))
return true;
}
}
}
}
}
return false;
}
private static (string file, int line) GetDeclSpan(ISymbol s)
{
var decl = s.DeclaringSyntaxReferences.FirstOrDefault();
if (decl == null) return ("", 0);
var node = decl.GetSyntax();
var ls = node.SyntaxTree.GetLineSpan(node.Span);
return (ls.Path ?? "", ls.StartLinePosition.Line + 1);
}
private static bool DerivesFrom(INamedTypeSymbol? type, string fullName)
{
for (var t = type; t != null; t = t.BaseType)
if (string.Equals(t.ToDisplayString(), fullName, StringComparison.Ordinal)) return true;
return false;
}
private static string Csv(string s)
{
if (s == null) return "";
if (s.Contains('"') || s.Contains(',') || s.Contains('\n') || s.Contains('\r'))
return $"\"{s.Replace("\"", "\"\"")}\"";
return s;
}
private static string Key(ISymbol s)
=> s.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "");
}
3.実行コマンド
Program.csをビルドしたら、Program.csを格納したフォルダ上でコマンドプロンプトを起動し、以下のコマンドを実行
dotnet run -- "D:\Path\YourSolution.sln" "YourNamespace.YourType.YourMethod" 5
名前空間.型名.メソッド名はVisual Studio上で確認可能。

第1引数: 解析する .sln のフルパス
第2引数: 対象メソッドの 完全修飾名(名前空間.型名.メソッド名)
第3引数: 上流に辿る 最大段数(省略可、既定 5)
出力(実行フォルダに作成)
top_callers.txt … 最上流の呼び出し元(= これ以上リポジトリで上流が見つからないメソッド)の一覧