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:
@@ -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
|
||||
Reference in New Issue
Block a user