"""참조 추출 — 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 --- 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