Paragraphとは
COBOLの Paragraph は、PROCEDURE DIVISIONに記述される処理の単位です。 名前(段落名)に続けてピリオドを置き、次の段落名が来るまでの文が1つのParagraphを構成します。 Javaのメソッドに最も近い概念で、Call Graphのノードになります。
PROCEDURE DIVISION.
* ─── Paragraph名(ノード)───────────────────────
MAIN-PARA.
PERFORM INIT-PARA *> エッジ: MAIN-PARA → INIT-PARA
PERFORM PROCESS-PARA *> エッジ: MAIN-PARA → PROCESS-PARA
PERFORM CLOSE-PARA *> エッジ: MAIN-PARA → CLOSE-PARA
STOP RUN.
INIT-PARA.
OPEN INPUT IN-FILE.
OPEN OUTPUT OUT-FILE.
PROCESS-PARA.
PERFORM READ-PARA *> エッジ: PROCESS-PARA → READ-PARA
PERFORM WRITE-PARA. *> エッジ: PROCESS-PARA → WRITE-PARA
READ-PARA.
READ IN-FILE AT END MOVE 'Y' TO WS-EOF.
WRITE-PARA.
WRITE OUT-REC FROM IN-REC.
CLOSE-PARA.
CLOSE IN-FILE OUT-FILE.
PERFORMの種類と解析難易度
| PERFORM形式 | 例 | 解析難易度 |
|---|---|---|
| 基本PERFORM | PERFORM PARA-A | ⭐ 簡単(1エッジ確定) |
| PERFORM UNTIL | PERFORM PARA-A UNTIL WS-FLG = 'Y' | ⭐ 簡単(条件は無視してエッジのみ) |
| PERFORM VARYING | PERFORM PARA-A VARYING I FROM 1 BY 1 UNTIL I > 10 | ⭐ 簡単(同上) |
| PERFORM THRU | PERFORM PARA-A THRU PARA-Z | ⭐⭐ 中(PARA-AからPARA-Zまでの連続Paragraphすべて) |
| インラインPERFORM | PERFORM ... END-PERFORM | ⭐ 簡単(内部に別Paragraph呼び出しがある場合のみ注意) |
| 動的CALL | CALL WS-PGM-NAME | ⭐⭐⭐ 困難(変数追跡が必要) |
⚠️ PERFORM THRU の注意点
PERFORM PARA-A THRU PARA-Z はソース上で PARA-A からPARA-Z の間に定義されたすべてのParagraphを順に実行します。そのため、抽出時にParagraphの定義順を保持しておく必要があります。
静的解析でCall Graphを構築する
Pythonで正規表現ベースの静的解析を実装します。 まずParagraph定義を収集し、次にPERFOM呼び出しを抽出してエッジを作成します。
import re
from pathlib import Path
from collections import defaultdict
def parse_paragraphs(text: str) -> list[str]:
"""PROCEDURE DIVISION 以降の Paragraph名を定義順に抽出"""
# PROCEDURE DIVISION 以降を切り出す
proc_match = re.search(r'PROCEDURE\s+DIVISION', text, re.IGNORECASE)
if not proc_match:
return []
proc_text = text[proc_match.start():]
# 段落名パターン: 7〜72桁目に記述される英数字+ハイフンの識別子+ピリオド
para_pattern = re.compile(
r'^ ([A-Z0-9][A-Z0-9\-]*)\.',
re.MULTILINE
)
paragraphs = para_pattern.findall(proc_text)
# PROCEDURE DIVISION 自体を除外
return [p for p in paragraphs if p != 'DIVISION']
def parse_perform_edges(text: str, paragraphs: list[str]) -> list[tuple]:
"""PERFORM文からCall Graphのエッジ(呼び出し元, 呼び出し先)を抽出"""
para_set = set(paragraphs)
edges = []
proc_match = re.search(r'PROCEDURE\s+DIVISION', text, re.IGNORECASE)
if not proc_match:
return []
proc_text = text[proc_match.start():]
# 現在の段落を追跡しながら PERFORM を検索
current_para = None
for line in proc_text.splitlines():
# 段落定義の検出
para_match = re.match(r'^ ([A-Z0-9][A-Z0-9\-]*)\.\s*$', line)
if para_match:
name = para_match.group(1)
if name != 'DIVISION':
current_para = name
continue
# PERFORM の検出
if current_para is None:
continue
perform_match = re.search(
r'\bPERFORM\s+([A-Z0-9][A-Z0-9\-]*)', line, re.IGNORECASE
)
if perform_match:
target = perform_match.group(1).upper()
# PERFORM UNTIL / VARYING / THROUGH などのキーワードは除外
keywords = {'UNTIL', 'VARYING', 'THROUGH', 'THRU', 'TIMES', 'WITH', 'TEST'}
if target not in keywords and target in para_set:
edges.append((current_para, target))
return edges
def build_callgraph(cobol_path: Path) -> dict:
text = cobol_path.read_text(encoding='utf-8', errors='replace').upper()
paragraphs = parse_paragraphs(text)
edges = parse_perform_edges(text, paragraphs)
graph = defaultdict(list)
for caller, callee in edges:
graph[caller].append(callee)
return {
'program': cobol_path.stem,
'paragraphs': paragraphs,
'edges': edges,
'graph': dict(graph),
}
# 使用例
result = build_callgraph(Path('SAMPLE-PGM.cbl'))
for caller, callees in result['graph'].items():
for callee in callees:
print(f'{caller} -> {callee}')
動的CALLの問題と対処
変数によるCALL(CALL WS-PGM-NAME)は静的解析では呼び出し先が確定しません。
以下の対処法を組み合わせます。
- MOVE文のトレース:
MOVE 'SUBPGM01' TO WS-PGM-NAMEのようなリテラル代入を追跡し、候補プログラム名を収集する - 実行ログの活用: 本番ジョブのSYSOUT・トレース出力から動的CALL先を収集する(動的解析的アプローチ)
- 未解決エッジとしてマーク: グラフ上で「動的CALL(要確認)」ノードを作り、手動で補完する
デッドコード検出への応用
Call Graphが完成すれば、入口Paragraph(MAIN-PARAなど)から到達できないParagraphを検出できます。 これがデッドコード(実行されない処理)の候補です。
from collections import deque
def find_dead_code(graph: dict, all_paragraphs: list, entry: str) -> list[str]:
"""到達不能なParagraphを返す(デッドコード候補)"""
visited = set()
queue = deque([entry])
while queue:
node = queue.popleft()
if node in visited:
continue
visited.add(node)
for callee in graph.get(node, []):
queue.append(callee)
dead = [p for p in all_paragraphs if p not in visited]
return dead
# 使用例
result = build_callgraph(Path('SAMPLE-PGM.cbl'))
dead = find_dead_code(result['graph'], result['paragraphs'], entry='MAIN-PARA')
print('デッドコード候補:', dead)
静的解析の限界
| ケース | 問題 | 対策 |
|---|---|---|
| 動的CALL | 呼び出し先が実行時まで不明 | MOVE文トレース・実行ログ活用 |
| PERFORM THRU | 範囲内Paragraph全体が対象 | Paragraph定義順を保持して範囲展開 |
| COPY句の展開内容 | コピーブック内のPERFOMを見落とす | COPY展開後テキストで解析 |
| ALTER文(廃止予定) | 実行時にGO TO先を書き換える | ALTER使用箇所をフラグして手動確認 |
✅ 次の章では…
PART 04 では実際のCOBOLソースから構造情報を抽出し、Jarvizが読み込める JSONLフォーマット に変換する実践スクリプトを解説します。