Files

288 lines
12 KiB
Python

"""정의부(선언 블록) 추출 — 조각 코드를 **붙여넣을 때 함께 가야 하는 코드**.
인덱스가 담는 코드 원문은 **로직만**이다. ABAP 은 내부테이블·스트럭처·상수·필드심볼을
TOP 인클루드(또는 FORM 머리)에 따로 선언하므로, 로직만 복사해 붙여넣으면 컴파일되지 않는다.
그래서 선언을 **별도로** 수집해 둔다. 무엇을 쓸지(이미 있으면 버리고, 이름이 겹치면 바꾸고)는
붙여넣는 쪽 LLM 이 정한다 — 여기서는 판단하지 않고 **원문 그대로, 붙여넣을 수 있는 형태로** 모은다.
parser/dataflow.py 의 `SymbolDecl` 과 겹쳐 보이지만 목적이 다르다:
- `symbol` : "gt_head 가 어디서 채워지는가" 추적용 — 이름·타입 문자열만 있으면 된다.
- `declaration` : 붙여넣기용 — **선언 원문**, **줄 범위**, **의존하는 다른 선언**이 필요하다.
그리고 `DATA: BEGIN OF ... END OF ...` 구조 선언을 반드시 한 덩어리로 잡아야 한다
(dataflow 는 이걸 건너뛴다).
"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from .statements import Statement
from .tokenizer import split_comment
# 선언 문장의 머리 키워드 → 선언 종류
DECL_HEADS = {
"TYPES": "types",
"DATA": "data",
"CLASS-DATA": "data",
"STATICS": "data",
"CONSTANTS": "constants",
"TABLES": "tables",
"NODES": "tables",
"FIELD-SYMBOLS": "field-symbol",
"PARAMETERS": "parameter",
"PARAMETER": "parameter", # 단수형도 유효한 ABAP 이다 (실측 119건)
"CONTROLS": "controls",
"SELECT-OPTIONS": "select-option",
"RANGES": "ranges",
"TYPE-POOLS": "type-pool",
"TYPE-POOL": "type-pool",
}
# TYPE/LIKE 뒤에서 건너뛰는 수식어 — 이 다음에 오는 식별자가 실제 참조 대상이다
TYPE_MODIFIERS = {
"STANDARD", "SORTED", "HASHED", "INDEX", "ANY", "TABLE", "OF", "LINE", "RANGE",
"REF", "TO", "OCCURS", "WITH", "HEADER", "INITIAL", "SIZE", "DEFAULT", "VALUE",
"READ-ONLY", "UNIQUE", "NON-UNIQUE", "KEY", "EMPTY", "LENGTH", "DECIMALS",
}
# 내장 타입 · 시스템 구조 — 참조로 기록하지 않는다 (어디에나 있으므로 붙여넣기에 방해만 된다)
BUILTIN_TYPES = {
"C", "N", "I", "F", "P", "D", "T", "X", "B", "S", "INT1", "INT2", "INT4", "INT8",
"STRING", "XSTRING", "DECFLOAT16", "DECFLOAT34", "UTCLONG", "FLOAT",
"SY", "SYST", "SPACE", "ABAP_BOOL", "ABAP_TRUE", "ABAP_FALSE",
}
_IDENT = re.compile(r"^[A-Z_/][A-Z0-9_/]*$")
@dataclass
class Declaration:
"""붙여넣기 가능한 선언 한 건."""
name: str
kind: str # types/data/constants/tables/field-symbol/parameter/select-option/
# ranges/type-pool/class/interface/macro
scope: str # global | unit
unit_id: str | None
include: str
line_start: int
line_end: int
code: str # 원문 (체인 항목이면 헤드를 다시 붙인 **유효한 한 문장**)
type_text: str = ""
depends: list[str] = field(default_factory=list) # 이 선언이 참조하는 다른 이름(대문자)
def base_name(token: str) -> str:
"""GS_HEAD-BELNR → GS_HEAD, GT_TAB[] → GT_TAB, <FS>-F → <FS>, ZCL_X=>TY → ZCL_X"""
t = token.upper()
if t.startswith("<"):
return t.split(">")[0] + ">"
t = t.split("(")[0].split("[")[0].split("->")[0].split("=>")[0].split("-")[0]
return t.rstrip(",.")
def _is_ident(t: str) -> bool:
return bool(_IDENT.match(t)) or (t.startswith("<") and t.endswith(">"))
def clean_ref(token: str) -> str:
"""참조 토큰을 정규화 — `GT_DATA[]` → GT_DATA, `BKPF-BELNR` → BKPF-BELNR (컴포넌트 유지)."""
t = token.upper().split("(")[0].split("[")[0].rstrip(",.")
return t
# 타입식의 머리 — 이 뒤에는 보통 `OF`/`TO` 로 실제 타입 이름이 이어진다 (없을 수도 있다)
_TABLE_HEADS = {"STANDARD", "SORTED", "HASHED", "INDEX", "ANY", "TABLE", "LINE", "RANGE", "REF"}
def type_name_after(up: list[str], i: int) -> tuple[str | None, int]:
"""`up[i]` 가 TYPE/LIKE 일 때 (참조하는 타입 이름, 다음에 읽을 위치).
타입식에 이름이 없을 수도 있다 — `TYPE ANY`, `TYPE ANY TABLE`, `TYPE C` 처럼.
그때 `None` 과 함께 **수식어 바로 다음 위치**를 돌려줘야 그 뒤 토큰(다음 FORM 파라미터 등)을
타입 이름으로 잘못 삼키지 않는다.
"""
j = i + 1
if j >= len(up):
return None, j
if up[j] not in _TABLE_HEADS:
return up[j], j + 1
k = j
while k < len(up) and up[k] not in {"OF", "TO"}:
if up[k] not in _TABLE_HEADS:
return None, k # `TYPE ANY` 뒤에 다른 토큰이 온 경우
k += 1
if k + 1 < len(up): # … OF <타입> / REF TO <클래스>
return up[k + 1], k + 2
return None, k
def type_refs(up: list[str]) -> list[str]:
"""선언 토큰열이 참조하는 다른 이름들 — TYPE/LIKE/INCLUDE STRUCTURE/FOR 뒤의 식별자.
`LIKE GT_DATA OCCURS 0` → [GT_DATA], `TYPE TABLE OF TY_ITEM` → [TY_ITEM],
`TYPE C` → [] (내장 타입).
**컴포넌트까지 그대로 남긴다**: `LIKE BKPF-BELNR` → [BKPF-BELNR]. 이건 DDIC 사전의 필드
타입을 빌려 쓰는 것이지 `TABLES: BKPF` 작업영역이 있어야 한다는 뜻이 아니다 — 정의부를 조립할
때(index/decls.py) 이 둘을 구분해야 TOP 의 TABLES 선언이 딸려 오지 않는다.
같은 이름을 여러 번 참조하면(구조체 필드들이 전부 같은 테이블을 볼 때) 첫 참조만 남긴다.
"""
out: list[str] = []
seen: set[str] = set()
def add(token: str) -> None:
ref = clean_ref(token)
base = base_name(ref)
if not _is_ident(base) or base in BUILTIN_TYPES or base in seen:
return
seen.add(base)
out.append(ref)
i = 0
while i < len(up):
t = up[i]
if t in {"TYPE", "LIKE"}:
name, nxt = type_name_after(up, i)
if name:
add(name)
i = nxt
continue
elif t == "INCLUDE" and i + 1 < len(up) and up[i + 1] in {"STRUCTURE", "TYPE"}:
if i + 2 < len(up):
add(up[i + 2])
i += 2
elif t == "FOR" and i + 1 < len(up): # SELECT-OPTIONS s_bukrs FOR bkpf-bukrs
add(up[i + 1])
i += 1
i += 1
return out
def _terminate(line: str) -> str:
"""체인에서 잘라낸 마지막 줄의 ',' 를 '.' 로 바꾼다 (주석은 보존)."""
code, comment, full = split_comment(line)
if full:
return line
stripped = code.rstrip()
if stripped.endswith(","):
stripped = stripped[:-1] + "."
elif not stripped.endswith("."):
stripped = stripped + "."
return stripped + (f' " {comment}' if comment else "")
def render_code(lines: list[str], st_start: int, st_end: int,
chain_head: str, chain_start: int, chain_end: int) -> str:
"""선언 원문을 **그대로 붙여넣을 수 있는 한 문장**으로 잘라낸다.
- 문장이 체인 전체와 같으면 원문 그대로 (이미 '.' 로 끝난다)
- 체인의 첫 항목이면 헤드가 첫 줄에 있으므로 그대로 잘라 '.' 로 닫는다
- 중간·마지막 항목이면 헤드(`DATA:`)가 없으므로 앞에 다시 붙인다
"""
body = [lines[no - 1] for no in range(st_start, st_end + 1) if 1 <= no <= len(lines)]
if not body:
return ""
if st_start == chain_start and st_end == chain_end:
return "\n".join(body).rstrip()
body[-1] = _terminate(body[-1])
if st_start > chain_start and chain_head:
return f"{chain_head}:\n" + "\n".join(body).rstrip()
return "\n".join(body).rstrip()
def _block_name(up: list[str]) -> str:
"""['DATA','BEGIN','OF','GT_UPLOAD','OCCURS','0'] → GT_UPLOAD"""
return base_name(up[3]) if len(up) >= 4 else ""
def extract_declaration_blocks(
statements: list[Statement], stmt_indexes: list[int], lines: list[str],
include: str, scope: str, unit_id: str | None = None,
) -> list[Declaration]:
"""선언 문장들 → Declaration 목록. `BEGIN OF … END OF` 는 한 덩어리로 묶는다."""
out: list[Declaration] = []
depth = 0
block: list[Statement] = []
block_kind = ""
block_name = ""
for idx in stmt_indexes:
st = statements[idx]
up = st.upper
if not up:
continue
kind = DECL_HEADS.get(up[0])
if kind is None:
# 구조 블록 안에서는 선언 키워드가 없는 문장(INCLUDE STRUCTURE …)도 함께 모은다
if depth and up[0] == "INCLUDE":
block.append(st)
continue
is_begin = len(up) >= 3 and up[1] == "BEGIN" and up[2] == "OF"
is_end = len(up) >= 3 and up[1] == "END" and up[2] == "OF"
if is_begin:
if depth == 0:
block, block_kind, block_name = [], kind, _block_name(up)
depth += 1
block.append(st)
continue
if depth:
block.append(st)
if is_end:
depth -= 1
if depth == 0 and block:
first, last = block[0], block[-1]
tokens: list[str] = []
for b in block:
tokens += b.upper
out.append(Declaration(
name=block_name or _block_name(first.upper), kind=block_kind, scope=scope,
unit_id=unit_id, include=include,
line_start=first.line_start, line_end=last.line_end,
code=render_code(lines, first.line_start, last.line_end,
first.chain_head, first.chain_start, last.chain_end),
type_text="BEGIN OF …",
depends=[d for d in type_refs(tokens) if base_name(d) != block_name],
))
block = []
continue
if len(up) < 2:
continue
if kind == "type-pool": # TYPE-POOLS: slis, vrm → 항목마다 한 건
name = base_name(up[1])
else:
name = base_name(up[1])
if not name or name in {"BEGIN", "END"} or not _is_ident(name):
continue
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
out.append(Declaration(
name=name, kind=kind, scope=scope, unit_id=unit_id, include=include,
line_start=st.line_start, line_end=st.line_end,
code=render_code(lines, st.line_start, st.line_end,
st.chain_head, st.chain_start, st.chain_end),
type_text=type_text,
depends=[d for d in type_refs(up) if base_name(d) != name],
))
return out
def unit_declaration(unit, lines: list[str], kind: str) -> Declaration:
"""CLASS … DEFINITION / DEFINE … END-OF-DEFINITION 처럼 **unit 통째가 정의부**인 것.
로컬 클래스를 쓰는 조각도, 매크로를 호출하는 조각도 그 정의가 없으면 붙여넣어 봐야 안 돈다.
"""
body = "\n".join(lines[unit.line_start - 1 : unit.line_end]).rstrip()
return Declaration(
name=unit.name.upper(), kind=kind, scope="global", unit_id=None, include=unit.include,
line_start=unit.line_start, line_end=unit.line_end, code=body,
type_text=kind, depends=[],
)