Java呼び出し元解析ツール(Python + config.yaml 対応)
特定のメソッドを呼び出しているJavaファイルを再帰的に解析し、最終的にどのプロジェクトで利用されているかをCSV形式で出力するツールです。
特徴
-
common
を起点に呼び出しをたどる - 呼び出し元のファイル・クラス・メソッド名を出力
- 最終呼び出し元が common を抜けた時点で解析終了
- 設定は
config.yaml
に集約
想定ディレクトリ構成
C:
└─ python
└─ 依存関係調査
├─ analyze_calls.py
└─ config.yaml
実行方法
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)