Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-21 13:23:37 +09:00
co-authored by Claude Fable 5.1
commit 11ae3629b2
453 changed files with 259183 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
"""참조 추출 — 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