Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
170 lines
7.4 KiB
Python
170 lines
7.4 KiB
Python
"""참조 추출 — unit 별 DB 접근/호출/권한/메시지/선택화면/UI 신호/텍스트심볼. (계획서 §4.3)"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
|
|
from .statements import Statement
|
|
|
|
_SQL_WRITE = {"INSERT", "UPDATE", "MODIFY", "DELETE"}
|
|
_UI_FM = re.compile(r"^(REUSE_ALV_|SSF_|FP_JOB_|GUI_DOWNLOAD|GUI_UPLOAD)")
|
|
|
|
# "외부 호출"로 볼 call kind. 요약·위키·엔티티 페이지가 같은 기준을 써야 한다.
|
|
# 제외하는 것들과 이유:
|
|
# perform / perform_unresolved — 프로그램 내부 호출
|
|
# call_method / method_ref — `CL_GUI_ALV_GRID=>MC_FC_AUF` 처럼 상수 읽기가 대량 섞인다
|
|
# call_screen — 대상이 화면번호('100')라 호출 대상 이름이 아니다
|
|
EXTERNAL_CALL_KINDS = (
|
|
"perform_external", "call_function", "call_function_rfc", "call_function_task",
|
|
"submit", "call_transaction", "call_dialog",
|
|
)
|
|
|
|
# functions/ 엔티티 페이지를 만드는 호출 종류 (FM · BAPI · RFC)
|
|
FUNCTION_CALL_KINDS = ("call_function", "call_function_rfc", "call_function_task")
|
|
|
|
|
|
@dataclass
|
|
class UnitRefs:
|
|
tables_read: list[str] = field(default_factory=list)
|
|
tables_write: list[str] = field(default_factory=list)
|
|
calls: list[dict] = field(default_factory=list) # {kind, target, extra?, line}
|
|
authority_checks: list[str] = field(default_factory=list)
|
|
messages: list[str] = field(default_factory=list)
|
|
select_params: list[dict] = field(default_factory=list) # {kind, name, for}
|
|
output_signals: list[str] = field(default_factory=list)
|
|
text_symbols: list[str] = field(default_factory=list)
|
|
macro_calls: list[str] = field(default_factory=list)
|
|
|
|
|
|
def _strip_quote(t: str) -> str:
|
|
return t[1:-1] if len(t) >= 2 and t[0] == "'" and t[-1] == "'" else t
|
|
|
|
|
|
def _is_name(t: str) -> bool:
|
|
return bool(re.match(r"^[A-Z_/][A-Z0-9_/]*$", t))
|
|
|
|
|
|
def extract_refs(statements: list[Statement], stmt_indexes: list[int],
|
|
macro_names: set[str], known_symbols: set[str]) -> UnitRefs:
|
|
r = UnitRefs()
|
|
|
|
def add(lst: list, v) -> None:
|
|
if v and v not in lst:
|
|
lst.append(v)
|
|
|
|
for idx in stmt_indexes:
|
|
st = statements[idx]
|
|
up = st.upper
|
|
if not up:
|
|
continue
|
|
head = up[0]
|
|
line = st.line_start
|
|
|
|
# --- DB 읽기: SELECT/OPEN CURSOR ... FROM t, JOIN t ---
|
|
if head in {"SELECT", "OPEN"} or (head == "WITH"):
|
|
for j, t in enumerate(up):
|
|
if t in {"FROM", "JOIN"} and j + 1 < len(up):
|
|
cand = up[j + 1]
|
|
if cand in {"(", "@"}:
|
|
continue
|
|
cand = cand.lstrip("@")
|
|
if _is_name(cand) and cand not in known_symbols:
|
|
add(r.tables_read, cand)
|
|
|
|
# --- DB 쓰기: INSERT/UPDATE/MODIFY/DELETE <db테이블> ---
|
|
if head in _SQL_WRITE and len(up) >= 2:
|
|
t1 = up[1]
|
|
if t1 == "INTO" and len(up) >= 3: # INSERT INTO t VALUES ...
|
|
t1 = up[2]
|
|
if t1 == "FROM" and head == "DELETE" and len(up) >= 3: # DELETE FROM t
|
|
t1 = up[2]
|
|
if _is_name(t1) and t1 not in known_symbols and t1 not in {"TABLE", "LINES", "ADJACENT", "REPORT", "DATASET", "SCREEN"}:
|
|
add(r.tables_write, t1)
|
|
|
|
# --- 호출 ---
|
|
if head == "PERFORM" and len(up) >= 2:
|
|
target = up[1]
|
|
m = re.match(r"^([A-Z0-9_]+)\(([A-Z0-9_]+)\)$", target) # 구문 f(prog)
|
|
if m:
|
|
r.calls.append({"kind": "perform_external", "target": m.group(1), "program": m.group(2), "line": line})
|
|
elif "IN" in up and "PROGRAM" in up:
|
|
pi = up.index("PROGRAM")
|
|
prog = up[pi + 1] if pi + 1 < len(up) else ""
|
|
r.calls.append({"kind": "perform_external", "target": target, "program": _strip_quote(prog), "line": line})
|
|
else:
|
|
r.calls.append({"kind": "perform", "target": target, "line": line})
|
|
|
|
if head == "CALL" and len(up) >= 2:
|
|
what = up[1]
|
|
if what == "FUNCTION" and len(up) >= 3:
|
|
fname = _strip_quote(up[2])
|
|
kind = "call_function"
|
|
if "DESTINATION" in up:
|
|
kind = "call_function_rfc"
|
|
add(r.output_signals, "RFC")
|
|
if "TASK" in up:
|
|
kind = "call_function_task"
|
|
r.calls.append({"kind": kind, "target": fname, "line": line})
|
|
if _UI_FM.match(fname):
|
|
add(r.output_signals, "ALV" if fname.startswith("REUSE_ALV_") else fname.split("_")[0])
|
|
if fname.startswith(("GUI_DOWNLOAD", "GUI_UPLOAD")):
|
|
add(r.output_signals, "FILE")
|
|
elif what == "METHOD" and len(up) >= 3:
|
|
r.calls.append({"kind": "call_method", "target": up[2], "line": line})
|
|
elif what == "TRANSACTION" and len(up) >= 3:
|
|
r.calls.append({"kind": "call_transaction", "target": _strip_quote(up[2]), "line": line})
|
|
if "USING" in up:
|
|
add(r.output_signals, "BDC")
|
|
elif what == "SCREEN" and len(up) >= 3:
|
|
r.calls.append({"kind": "call_screen", "target": up[2], "line": line})
|
|
add(r.output_signals, "SCREEN")
|
|
elif what == "DIALOG" and len(up) >= 3:
|
|
r.calls.append({"kind": "call_dialog", "target": _strip_quote(up[2]), "line": line})
|
|
|
|
if head == "SUBMIT" and len(up) >= 2:
|
|
r.calls.append({"kind": "submit", "target": up[1], "line": line})
|
|
|
|
# 함수형 메서드 호출: zcl_x=>meth( ... ) / lo_obj->meth( ... )
|
|
for t in st.tokens:
|
|
tu = t.upper()
|
|
if "=>" in tu or "->" in tu:
|
|
base = tu.split("(")[0]
|
|
if re.match(r"^[A-Z0-9_/<>]+(=>|->)[A-Z0-9_~]+$", base):
|
|
if not any(c["target"] == base for c in r.calls):
|
|
r.calls.append({"kind": "method_ref", "target": base, "line": line})
|
|
if base.startswith("CL_GUI_ALV_GRID") or "ALV" in base.split("=>")[0].split("->")[0]:
|
|
add(r.output_signals, "ALV_GRID")
|
|
|
|
# --- 권한/메시지 ---
|
|
if head == "AUTHORITY-CHECK" and "OBJECT" in up:
|
|
oi = up.index("OBJECT")
|
|
if oi + 1 < len(up):
|
|
add(r.authority_checks, _strip_quote(up[oi + 1]))
|
|
if head == "MESSAGE" and len(up) >= 2:
|
|
add(r.messages, _strip_quote(up[1])[:40])
|
|
|
|
# --- 선택화면 ---
|
|
if head == "PARAMETERS" and len(up) >= 2:
|
|
r.select_params.append({"kind": "parameter", "name": up[1].split("(")[0], "line": line})
|
|
if head == "SELECT-OPTIONS" and len(up) >= 2:
|
|
target = ""
|
|
if "FOR" in up:
|
|
fi = up.index("FOR")
|
|
target = up[fi + 1] if fi + 1 < len(up) else ""
|
|
r.select_params.append({"kind": "select_option", "name": up[1], "for": target, "line": line})
|
|
|
|
# --- UI 신호 ---
|
|
if head == "WRITE":
|
|
add(r.output_signals, "LIST_WRITE")
|
|
if head == "CALL" and len(up) >= 3 and up[1] == "SCREEN":
|
|
add(r.output_signals, "SCREEN")
|
|
|
|
# --- 텍스트 심볼 / 매크로 ---
|
|
for t in up:
|
|
if t.startswith("TEXT-"):
|
|
add(r.text_symbols, t.replace("TEXT-", ""))
|
|
if head in macro_names:
|
|
add(r.macro_calls, head)
|
|
|
|
return r
|