"""심볼 선언 + 쓰기/읽기 지점 추출. (계획서 §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, -f → """ 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"^[-comp / ->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"^[= 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"^[ 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 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"^[= 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"^[= 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 = ... / -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"^[