Paragraphとは

COBOLの Paragraph は、PROCEDURE DIVISIONに記述される処理の単位です。 名前(段落名)に続けてピリオドを置き、次の段落名が来るまでの文が1つのParagraphを構成します。 Javaのメソッドに最も近い概念で、Call Graphのノードになります。

COBOL — Paragraphの定義
       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形式解析難易度
基本PERFORMPERFORM PARA-A⭐ 簡単(1エッジ確定)
PERFORM UNTILPERFORM PARA-A UNTIL WS-FLG = 'Y'⭐ 簡単(条件は無視してエッジのみ)
PERFORM VARYINGPERFORM PARA-A VARYING I FROM 1 BY 1 UNTIL I > 10⭐ 簡単(同上)
PERFORM THRUPERFORM PARA-A THRU PARA-Z⭐⭐ 中(PARA-AからPARA-Zまでの連続Paragraphすべて)
インラインPERFORMPERFORM ... END-PERFORM⭐ 簡単(内部に別Paragraph呼び出しがある場合のみ注意)
動的CALLCALL WS-PGM-NAME⭐⭐⭐ 困難(変数追跡が必要)

⚠️ PERFORM THRU の注意点

PERFORM PARA-A THRU PARA-Z はソース上で PARA-A からPARA-Z の間に定義されたすべてのParagraphを順に実行します。そのため、抽出時にParagraphの定義順を保持しておく必要があります。

静的解析でCall Graphを構築する

Pythonで正規表現ベースの静的解析を実装します。 まずParagraph定義を収集し、次にPERFOM呼び出しを抽出してエッジを作成します。

Python — Paragraph Call Graph 構築
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を検出できます。 これがデッドコード(実行されない処理)の候補です。

Python — デッドコード検出
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フォーマット に変換する実践スクリプトを解説します。

→ PART 04 実践:JSONL変換へ