1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.NET用の呼び出し階層の一覧化

Last updated at Posted at 2025-09-03

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;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using 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.Method> [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 FindMethodSymbolAsync(solution, targetFqn);
            if (target == null)
            {
                Console.WriteLine($"対象メソッドが見つかりません: {targetFqn}");
                return 3;
            }

            var completed = new List<List<IMethodSymbol>>();
            await ExploreUpstreamAsync(
                target, solution, maxDepth,
                new Stack<IMethodSymbol>(), 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 async Task<IMethodSymbol?> FindMethodSymbolAsync(Solution solution, string fqn)
    {
        var lastDot = fqn.LastIndexOf('.');
        if (lastDot < 0) return null;
        var typeFqn = fqn.Substring(0, lastDot);
        var methodName = fqn.Substring(lastDot + 1);

        foreach (var project in solution.Projects)
        {
            var compilation = await project.GetCompilationAsync();
            if (compilation == null) continue;

            var type = compilation.GetTypeByMetadataName(typeFqn);
            if (type != null)
            {
                var method = type.GetMembers().OfType<IMethodSymbol>()
                    .FirstOrDefault(m => m.Name == methodName);
                if (method != null) return method;
            }
        }

        foreach (var project in solution.Projects)
        {
            foreach (var doc in project.Documents)
            {
                var model = await doc.GetSemanticModelAsync();
                var root = await doc.GetSyntaxRootAsync();
                if (model == null || root == null) continue;

                foreach (var node in root.DescendantNodes())
                {
                    if (model.GetDeclaredSymbol(node) is IMethodSymbol sym)
                    {
                        var candTypeFqn = sym.ContainingType?
                            .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
                            ?.Replace("global::", "");
                        if (candTypeFqn == typeFqn && sym.Name == methodName)
                            return sym;
                    }
                }
            }
        }
        return null;
    }

    private static async Task ExploreUpstreamAsync(
        IMethodSymbol callee,
        Solution solution,
        int depth,
        Stack<IMethodSymbol> current,
        List<List<IMethodSymbol>> completed,
        HashSet<string> pathVisited)
    {
        var callers = await SymbolFinder.FindCallersAsync(callee, solution);
        var callerSyms = callers.Select(c => c.CallingSymbol)
                                .OfType<IMethodSymbol>()
                                .Where(IsFromSource)
                                .Where(IsInteresting)
                                .ToList();

        if (callerSyms.Count == 0 || depth <= 0)
        {
            var path = current.Reverse().ToList();
            path.Add(callee);
            completed.Add(path);
            return;
        }

        foreach (var caller in callerSyms)
        {
            var key = Key(caller);
            if (pathVisited.Contains(key)) continue;

            pathVisited.Add(key);
            current.Push(caller);
            await ExploreUpstreamAsync(caller, solution, depth - 1, current, completed, pathVisited);
            current.Pop();
            pathVisited.Remove(key);
        }
    }

    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;
    }

    private static void WriteTopCallersCsv_Dedup(
        List<List<IMethodSymbol>> completed,
        IMethodSymbol target,
        Solution solution,
        string file)
    {
        var byTop = new Dictionary<string, List<IMethodSymbol>>(StringComparer.Ordinal);

        foreach (var path 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.Count)
                byTop[topKey] = path;
        }

        using var sw = new StreamWriter(file, false);
        sw.WriteLine("Namespace,File,Call,LineStart,IsEntryPoint,CallHierarchy");

        foreach (var kv in byTop.OrderBy(k => k.Key))
        {
            var path = kv.Value;
            var topMost = path.Count >= 2 ? path[path.Count - 2] : path.First();

            var ns = topMost.ContainingNamespace?.ToDisplayString() ?? "";
            var (fullPath, start) = GetMethodDeclSpan(topMost);

            bool isEntry = IsEntryPoint(topMost, solution).GetAwaiter().GetResult();

            var chainParts = new List<string> { target.Name };
            for (int i = 0; i <= path.Count - 2; i++)
                chainParts.Add(path[i].Name);
            var chain = string.Join("<", chainParts);

            sw.WriteLine($"{Csv(ns)},{Csv(fullPath)},{Csv(Key(topMost))},{start},{(isEntry ? "True" : "False")},{Csv(chain)}");
        }
    }

    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 MethodBlockSyntax mb && mb.SubOrFunctionStatement?.HandlesClause != null)
                    return true;
                if (node is 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;
    }

    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;
                        }
                    }
                }

                if (doc.Project.Language == LanguageNames.CSharp)
                {
                    foreach (var assign in root.DescendantNodes()
                                .OfType<AssignmentExpressionSyntax>())
                    {
                        if (assign.Kind() != Microsoft.CodeAnalysis.CSharp.SyntaxKind.AddAssignmentExpression) continue;
                        var rightSym = model.GetSymbolInfo(assign.Right).Symbol as IMethodSymbol;
                        if (rightSym != null &&
                            SymbolEqualityComparer.Default.Equals(rightSym.OriginalDefinition, m.OriginalDefinition))
                            return true;
                    }
                }
            }
        }
        return false;
    }

    private static (string file, int line) GetMethodDeclSpan(IMethodSymbol m)
    {
        var decl = m.DeclaringSyntaxReferences.FirstOrDefault();
        if (decl == null) return ("", 0);

        var node = decl.GetSyntax();
        SyntaxNode spanNode = node;

        if (node.Language == LanguageNames.VisualBasic)
        {
            if (node is MethodBlockSyntax mb) spanNode = mb;
            else if (node is MethodStatementSyntax ms)
                spanNode = ms.Parent is MethodBlockSyntax pmb ? pmb : ms;
        }
        else if (node.Language == LanguageNames.CSharp)
        {
            if (node is MethodDeclarationSyntax mds) spanNode = mds;
        }

        var ls = spanNode.SyntaxTree.GetLineSpan(spanNode.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.実行コマンド

dotnet run -- "D:\Path\YourSolution.sln" "YourNamespace.YourType.YourMethod" 5

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?