Java呼び出し元解析ツール（Python + config.yaml 対応）

# Java呼び出し元解析ツール（Python + config.yaml 対応）

特定のメソッドを呼び出しているJavaファイルを再帰的に解析し、最終的にどのプロジェクトで利用されているかをCSV形式で出力するツールです。

## 特徴
- `common` を起点に呼び出しをたどる
- 呼び出し元のファイル・クラス・メソッド名を出力
- 最終呼び出し元が common を抜けた時点で解析終了
- 設定は `config.yaml` に集約

## 想定ディレクトリ構成

C:
└─ python
└─ 依存関係調査
├─ analyze_calls.py
└─ config.yaml

## 実行方法
```bash

import os
import re
import csv
from pathlib import Path
from collections import defaultdict, deque

# === 設定 ===
PROJECT_ROOT = Path("C:/workspace_abc/6末")
COMMON_PROJECT = "ava-common"
TARGET_METHOD = "selectByMemberId"
TARGET_DAOS = {"aaaDao", "aaaBaseDao"}
EXCLUDE_PROJECTS = {"temp-project", "legacy"}
OUTPUT_FILE = "calls_cross_class.csv"
# =============

def get_all_java_files():
    return list(PROJECT_ROOT.rglob("*.java"))

def extract_project_name(path: Path):
    for part in path.parts:
        if (PROJECT_ROOT / part).exists():
            return part
    return "unknown"

def extract_class_and_methods(file_path):
    class_name = file_path.stem
    methods = {}
    var_types = {}
    try:
        with open(file_path, encoding="utf-8", errors="ignore") as f:
            lines = f.readlines()
            current_method = None
            brace_count = 0
            for i, line in enumerate(lines):
                # クラス名取得
                class_match = re.search(r'\b(class|interface|enum)\s+(\w+)', line)
                if class_match:
                    class_name = class_match.group(2)

                # フィールド・ローカル変数からクラス名と変数名を取得
                var_match = re.findall(r'\b(\w+)\s+(\w+)\s*(=|;)', line)
                for vtype, vname, _ in var_match:
                    if vtype != "return" and len(vname) < 50:
                        var_types[vname] = vtype

                # メソッド定義取得
                def_match = re.match(r'\s*(public|private|protected)?\s+\w[\w<>\[\]]*\s+(\w+)\s*\([^;]*\)\s*\{?', line)
                if def_match:
                    current_method = def_match.group(2)
                    methods[current_method] = {
                        "start": i,
                        "calls": [],
                        "file": file_path,
                        "class": class_name,
                        "line_text": [],
                        "vars": var_types.copy()
                    }
                    brace_count = line.count("{") - line.count("}")
                    continue
                if current_method:
                    brace_count += line.count("{") - line.count("}")
                    if brace_count < 0:
                        current_method = None
                        continue
                    methods[current_method]["line_text"].append(line.strip())
                    call_match = re.findall(r'(\w+)\.(\w+)\s*\(', line)
                    if call_match:
                        methods[current_method]["calls"].extend(call_match)
    except Exception:
        pass
    return class_name, methods

def build_class_method_map(java_files):
    class_to_file = {}
    all_methods = {}
    for file in java_files:
        class_name, methods = extract_class_and_methods(file)
        class_to_file[class_name] = file
        for method_name, meta in methods.items():
            all_methods[(class_name, method_name)] = meta
    return class_to_file, all_methods

def method_calls_target_dao(method_info):
    joined = " ".join(method_info["line_text"])
    for dao in TARGET_DAOS:
        pattern = rf'\b(?:this\.)?{re.escape(dao)}\.{re.escape(TARGET_METHOD)}\s*\('
        if re.search(pattern, joined):
            return True
    return False

def find_entry_methods(all_methods):
    return [(cls, mname) for (cls, mname), meta in all_methods.items() if method_calls_target_dao(meta)]

def build_call_chains(entries, all_methods):
    chains = []
    for cls, mname in entries:
        visited = set()
        queue = deque()
        queue.append([(cls, mname)])
        while queue:
            path = queue.popleft()
            last_cls, last_mth = path[-1]
            visited.add((last_cls, last_mth))
            for (cand_cls, cand_mth), meta in all_methods.items():
                for var, called in meta["calls"]:
                    called_cls = meta["vars"].get(var)
                    if called_cls == last_cls and called == last_mth:
                        if (cand_cls, cand_mth) in visited:
                            continue
                        new_path = path + [(cand_cls, cand_mth)]
                        project = extract_project_name(meta["file"])
                        if COMMON_PROJECT not in str(meta["file"]) and project not in EXCLUDE_PROJECTS:
                            chains.append(new_path)
                        else:
                            queue.append(new_path)
    return chains, all_methods

def safe_str(s):
    return str(s).encode("utf-8", errors="replace").decode("utf-8")

def write_chains_to_csv(chains, method_map):
    max_depth = max(len(c) for c in chains) if chains else 0
    headers = []
    for i in range(max_depth):
        headers += [f"呼び出し元{i+1}_ファイル", f"呼び出し元{i+1}_クラス", f"呼び出し元{i+1}_メソッド"]
    headers += ["最終呼び出し元ファイル", "プロジェクト名", "呼び出し種別"]

    with open(OUTPUT_FILE, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.writer(f)
        writer.writerow(headers)
        for chain in chains:
            row = []
            for cls, mth in chain:
                meta = method_map[(cls, mth)]
                row += [safe_str(meta["file"].name), safe_str(meta["class"]), safe_str(mth)]
            pad = (max_depth - len(chain)) * ["", "", ""]
            final_file = method_map[chain[-1]].get("file", "")
            project = extract_project_name(final_file)
            call_type = "直接" if COMMON_PROJECT not in str(method_map[chain[0]].get("file", "")) else "common経由"
            row += [safe_str(final_file.name), safe_str(project), call_type]
            writer.writerow(row)

# 実行
java_files = get_all_java_files()
class_map, method_map = build_class_method_map(java_files)
entries = find_entry_methods(method_map)
chains, full_methods = build_call_chains(entries, method_map)
write_chains_to_csv(chains, full_methods)