"""정의부(선언 블록) 추출 — 조각 코드를 **붙여넣을 때 함께 가야 하는 코드**. 인덱스가 담는 코드 원문은 **로직만**이다. 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, -F → , 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=[], )