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
+327
View File
@@ -0,0 +1,327 @@
"""심볼 선언 + 쓰기/읽기 지점 추출. (계획서 §4.4)
"gt_head 가 어디서 채워지는가" 질문의 근간. 정확한 의미 분석이 아니라
문장 패턴별 쓰기 대상 변수를 결정론적으로 뽑는다.
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from .declarations import type_name_after
from .statements import Statement, assignment_eq_index
_NAME = re.compile(r"^[A-Za-z_/][A-Za-z0-9_/]*")
@dataclass
class SymbolDecl:
name: str
scope: str # global | unit
unit_id: str | None
decl_include: str
decl_line: int
type_text: str = ""
ddic_ref: str = ""
kind: str = "data" # data/types/constants/tables/field-symbol/parameter/select-option
@dataclass
class WritePoint:
symbol: str
unit_id: str
include: str
line: int
kind: str
source_symbols: list[str] = field(default_factory=list)
source_tables: list[str] = field(default_factory=list)
stmt_text: str = ""
callee: str = "" # kind=via_perform 일 때
def base_symbol(token: str) -> str:
"""gs_head-belnr → GS_HEAD, gt_tab[] → GT_TAB, <fs>-f → <FS>"""
t = token.upper()
if t.startswith("<"):
return t.split(">")[0] + ">"
t = t.split("(")[0].split("[")[0].split("-")[0].split("->")[0].split("=>")[0]
return t
def _decl_from_stmt(st: Statement, unit_id: str | None, scope: str) -> SymbolDecl | None:
up = st.upper
head = up[0]
kind_map = {
"DATA": "data", "TYPES": "types", "CONSTANTS": "constants", "STATICS": "data",
"TABLES": "tables", "FIELD-SYMBOLS": "field-symbol", "CLASS-DATA": "data",
"PARAMETERS": "parameter", "PARAMETER": "parameter",
"SELECT-OPTIONS": "select-option", "RANGES": "data",
"NODES": "tables",
}
if head not in kind_map or len(up) < 2:
return None
name = base_symbol(up[1])
if not name or name in {"BEGIN", "END"}: # DATA BEGIN OF ... 구조 선언은 v1 스킵
return None
type_text = ""
for kw in ("TYPE", "LIKE"):
if kw in up:
ki = up.index(kw)
type_text = " ".join(st.tokens[ki : ki + 5])[:120]
break
ddic_ref = ""
m = re.search(r"(?:TYPE|LIKE)\s+(?:TABLE\s+OF\s+|STANDARD\s+TABLE\s+OF\s+|SORTED\s+TABLE\s+OF\s+|HASHED\s+TABLE\s+OF\s+)?([A-Z0-9_/]+-?[A-Z0-9_/]*)", type_text.upper())
if m:
ddic_ref = m.group(1)
return SymbolDecl(
name=name, scope=scope, unit_id=unit_id, decl_include=st.include,
decl_line=st.line_start, type_text=type_text, ddic_ref=ddic_ref, kind=kind_map[head],
)
def extract_declarations(statements: list[Statement], stmt_indexes: list[int],
unit_id: str | None, scope: str,
unit_type: str = "", signature: str = "") -> list[SymbolDecl]:
decls: list[SymbolDecl] = []
for idx in stmt_indexes:
d = _decl_from_stmt(statements[idx], unit_id, scope)
if d:
decls.append(d)
# FORM 파라미터 (USING/CHANGING/TABLES)
#
# 타입을 붙인 파라미터가 여러 개면(`USING p_date LIKE x p_days LIKE y …`) **타입 이름만 건너뛰고
# 계속 읽어야** 한다. 예전 구현은 TYPE/LIKE 를 만나면 멈춰서 첫 파라미터만 잡았고, 나머지는
# 선언 없는 이름으로 남았다 (정의부 조립에서 '선언을 못 찾음'으로 새어 나와 드러났다).
if unit_type == "FORM" and signature:
sig_up = signature.upper().split()
section = ""
i = 0
while i < len(sig_up):
t = sig_up[i].strip(".")
if t in {"USING", "CHANGING", "TABLES"}:
section = t
elif t in {"TYPE", "LIKE", "STRUCTURE"}:
_type, i = type_name_after([x.strip(".") for x in sig_up], i)
continue # 타입식(이름이 없을 수도 있다)을 건너뛴다
elif t in {"VALUE", "REFERENCE", "(", ")"}:
pass # VALUE(p_x) 표기
elif section:
name = base_symbol(t)
if _NAME.match(name):
decls.append(SymbolDecl(name=name, scope="unit", unit_id=unit_id,
decl_include="", decl_line=0, kind="param"))
i += 1
return decls
_ASSIGN_KINDS = [
# (조건 함수, kind, 대상 위치 결정)
# 아래 extract_writes 에서 절차적으로 처리
]
_CLEAR_HEADS = {"CLEAR": "clear", "REFRESH": "clear", "FREE": "clear", "SORT": "sort"}
def extract_writes(statements: list[Statement], stmt_indexes: list[int], unit_id: str,
known_symbols: set[str]) -> tuple[list[WritePoint], list[dict]]:
"""(writes, reads). reads 는 {symbol, unit_id, include, line} 수준."""
writes: list[WritePoint] = []
reads: list[dict] = []
def known(sym: str) -> bool:
return sym in known_symbols
def add_write(sym_token: str, st: Statement, kind: str, sources: list[str] | None = None,
tables: list[str] | None = None, callee: str = "") -> None:
sym = base_symbol(sym_token)
if not sym or not re.match(r"^[<A-Z_/]", sym):
return
field = ""
tu = sym_token.upper()
if tu.startswith("<"):
# <fs>-comp / <fs>->attr — 필드심볼 이름(<...>) 뒤의 접근자만 떼낸다
rest = tu.split(">", 1)[1] if ">" in tu else ""
field = rest.lstrip("->")
elif "-" in tu:
field = tu.split("-", 1)[1]
wp = WritePoint(symbol=sym, unit_id=unit_id, include=st.include, line=st.line_start,
kind=kind if not field else f"{kind}:field={field[:30]}",
source_symbols=sorted({s for s in (sources or []) if known(s)}),
source_tables=sorted(set(tables or [])),
stmt_text=st.raw_text[:200], callee=callee)
writes.append(wp)
def add_read(sym: str, st: Statement) -> None:
if known(sym):
reads.append({"symbol": sym, "unit_id": unit_id, "include": st.include, "line": st.line_start})
for idx in stmt_indexes:
st = statements[idx]
up = st.upper
if not up:
continue
head = up[0]
rhs_syms = [base_symbol(t) for t in up[1:] if re.match(r"^[<A-Z_/]", t)]
# SELECT ... INTO [CORRESPONDING FIELDS OF] [TABLE] x / APPENDING TABLE x
if head in {"SELECT", "FETCH"}:
tables = []
for j, t in enumerate(up):
if t in {"FROM", "JOIN"} and j + 1 < len(up):
cand = up[j + 1].lstrip("@")
if re.match(r"^[A-Z0-9_/]+$", cand):
tables.append(cand)
for kw in ("INTO", "APPENDING"):
if kw in up:
j = up.index(kw)
k = j + 1
while k < len(up) and up[k] in {"CORRESPONDING", "FIELDS", "OF", "TABLE", "(", "@"}:
k += 1
if k < len(up):
add_write(up[k].lstrip("@("), st, "select", tables=tables)
break
continue
# APPEND ... TO x / INSERT ... INTO [TABLE] x / COLLECT ... INTO x
if head in {"APPEND", "COLLECT"}:
kw = "TO" if "TO" in up else ("INTO" if "INTO" in up else None)
if kw:
j = up.index(kw)
if j + 1 < len(up):
add_write(up[j + 1], st, "append", sources=rhs_syms)
elif len(up) >= 2: # APPEND x. (헤더라인)
add_write(up[1], st, "append", sources=rhs_syms)
continue
if head == "INSERT" and ("INTO" in up):
j = up.index("INTO")
k = j + 1
if k < len(up) and up[k] == "TABLE":
k += 1
if k < len(up) and base_symbol(up[k]) in known_symbols:
add_write(up[k], st, "insert", sources=rhs_syms)
continue
# 아니면 DB 쓰기 → refs 에서 처리
if head == "MODIFY" and len(up) >= 2 and base_symbol(up[1]) in known_symbols:
add_write(up[1], st, "modify", sources=rhs_syms)
continue
if head == "DELETE" and len(up) >= 2:
t1 = up[1]
if t1 == "ADJACENT" and "FROM" in up:
j = up.index("FROM")
if j + 1 < len(up):
add_write(up[j + 1], st, "delete")
continue
if base_symbol(t1) in known_symbols:
add_write(t1, st, "delete")
continue
if head in _CLEAR_HEADS:
for t in up[1:]:
if re.match(r"^[<A-Z_/]", t) and t not in {"BY", "ASCENDING", "DESCENDING", "STABLE", "WITH", "INITIAL", "LINE", "OF", "TABLE"}:
add_write(t, st, _CLEAR_HEADS[head])
continue
# READ TABLE ... INTO x / ASSIGNING <fs>
if head == "READ" and len(up) >= 2 and up[1] == "TABLE":
src = base_symbol(up[2]) if len(up) >= 3 else ""
if src:
add_read(src, st)
for kw, kind in (("INTO", "read_into"), ("ASSIGNING", "assign_fs")):
if kw in up:
j = up.index(kw)
if j + 1 < len(up):
add_write(up[j + 1], st, kind, sources=[src] if src else [])
continue
# LOOP AT x INTO y / ASSIGNING <fs>
if head == "LOOP" and "AT" in up:
j = up.index("AT")
src = base_symbol(up[j + 1]) if j + 1 < len(up) else ""
if src:
add_read(src, st)
for kw in ("INTO", "ASSIGNING"):
if kw in up:
k = up.index(kw)
if k + 1 < len(up):
add_write(up[k + 1], st, "iterate", sources=[src] if src else [])
continue
# MOVE / MOVE-CORRESPONDING ... TO x
if head in {"MOVE", "MOVE-CORRESPONDING"} and "TO" in up:
j = up.index("TO")
if j + 1 < len(up):
add_write(up[j + 1], st, "assign", sources=rhs_syms[:j])
continue
# SPLIT/CONCATENATE ... INTO x1 x2...
if head in {"SPLIT", "CONCATENATE"} and "INTO" in up:
j = up.index("INTO")
for t in up[j + 1 :]:
if t in {"SEPARATED", "BY", "IN", "CHARACTER", "BYTE", "MODE", "TABLE"}:
continue
if re.match(r"^[<A-Z_/]", t):
add_write(t, st, "assign", sources=rhs_syms[:j])
continue
# CALL FUNCTION ... IMPORTING a = x / TABLES t = x / CHANGING c = x
if head == "CALL" and len(up) >= 3 and up[1] == "FUNCTION":
fname = up[2].strip("'")
section = ""
k = 3
while k < len(up):
t = up[k]
if t in {"EXPORTING", "IMPORTING", "TABLES", "CHANGING", "EXCEPTIONS", "DESTINATION", "STARTING", "PERFORMING"}:
section = t
elif t == "=" and k + 1 < len(up) and section in {"IMPORTING", "TABLES", "CHANGING"}:
add_write(up[k + 1], st, "call_function", callee=fname)
elif t == "=" and k + 1 < len(up) and section == "EXPORTING":
add_read(base_symbol(up[k + 1]), st)
k += 1
continue
# PERFORM f USING a CHANGING x / TABLES x
if head == "PERFORM":
callee = up[1] if len(up) >= 2 else ""
section = ""
for t in up[2:]:
if t in {"USING", "CHANGING", "TABLES", "IN", "PROGRAM", "IF", "FOUND"}:
section = t
continue
if section in {"CHANGING", "TABLES"} and re.match(r"^[<A-Z_/]", t):
add_write(t, st, "via_perform", callee=callee)
elif section == "USING" and re.match(r"^[<A-Z_/]", t):
add_read(base_symbol(t), st)
continue
# IMPORT ... FROM MEMORY / GET PARAMETER ID ... FIELD x
if head == "IMPORT" and "FROM" in up:
j = up.index("FROM")
for t in up[1:j]:
if t not in {"TO", "="} and re.match(r"^[<A-Z_/]", t):
add_write(t, st, "import_memory")
continue
if head == "GET" and len(up) >= 2 and up[1] == "PARAMETER" and "FIELD" in up:
j = up.index("FIELD")
if j + 1 < len(up):
add_write(up[j + 1], st, "get_parameter")
continue
# 일반 대입: x = ... / x[] = ... / x-f = ... / <fs>-f = ...
eq = assignment_eq_index(up)
if eq is not None:
rhs = up[eq + 1 :]
kind = "assign"
joined = " ".join(rhs)
if "VALUE #(" in joined or (rhs and rhs[0].startswith("VALUE")):
kind = "assign_value"
elif rhs and rhs[0].startswith("CORRESPONDING"):
kind = "assign_corresponding"
add_write(up[0], st, kind, sources=[base_symbol(t) for t in rhs if re.match(r"^[<A-Z_/]", t)])
continue
# 그 밖의 문장에서 알려진 심볼 등장 → read
for t in up:
b = base_symbol(t)
if known(b):
add_read(b, st)
return writes, reads