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:
+237
@@ -0,0 +1,237 @@
|
||||
"""정의부 조립 — 조각(또는 unit) 코드를 붙여넣을 때 함께 가야 하는 선언을 모은다.
|
||||
|
||||
인덱스에 든 코드 원문은 **로직만**이다. ABAP 은 내부테이블·스트럭처·상수·필드심볼을 TOP 인클루드나
|
||||
FORM 머리에 따로 선언하므로, 로직만 복사하면 그대로는 컴파일되지 않는다. 여기서 하는 일:
|
||||
|
||||
1. 조각 코드에 등장하는 이름을 모은다 (파서 토크나이저)
|
||||
2. 그 이름의 선언을 `declaration` 테이블에서 찾는다 (unit 로컬 → 전역 순)
|
||||
3. 선언이 참조하는 다른 선언을 **재귀로** 끌어온다
|
||||
(`LT_DATA LIKE GT_DATA` → GT_DATA, `GT_ITEMS TYPE TY_ITEMS` → TY_ITEMS → TY_ITEM)
|
||||
4. 의존이 먼저 오도록 정렬해 **붙여넣을 수 있는 한 덩어리**로 만든다
|
||||
|
||||
**쓸지 말지는 판단하지 않는다.** 붙여넣는 쪽 LLM 이 정한다(이미 있으면 버리고, 이름이 겹치면 바꾸고).
|
||||
그래서 분류만 붙여 넘긴다: 가져가야 할 선언 / DDIC 외부 참조 / FORM 파라미터 / 못 찾은 이름.
|
||||
|
||||
조각→선언 대응을 따로 저장하지 않고 **읽을 때 계산**한다. 결정론적이라 언제 계산해도 같고,
|
||||
조각이 재추출되거나 선언이 바뀌어도 엇갈리지 않는다 (logic_chunk 에 캐시하면 둘이 따로 늙는다).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sqlite3
|
||||
|
||||
from parser.declarations import BUILTIN_TYPES, base_name, clean_ref, type_name_after
|
||||
from parser.statements import split_statements
|
||||
|
||||
from .db import loads
|
||||
|
||||
MAX_DECLS = 80 # 이 이상은 붙여넣기용으로 의미가 없다 (TOP 전체를 퍼오는 꼴)
|
||||
MAX_CODE_LINES = 500
|
||||
|
||||
# 선언을 못 찾았을 때 "변수처럼 생겼는가" 판정 — 한국 SAP 관행의 접두사(GV_/LT_/LS_/P_ …).
|
||||
# DDIC 이름(LVC_T_FCAT, W3_QVALUE)이나 키워드가 섞여 들어오지 않게 접두사를 2자 이내로 제한한다.
|
||||
_VARLIKE = re.compile(r"^[A-Z]{1,2}_[A-Z0-9_]*$")
|
||||
_FIELD_SYMBOL = re.compile(r"^<[A-Z0-9_]+>$")
|
||||
# 전역 리포지토리 객체(클래스·인터페이스·예외) — 프로그램 선언이 아니므로 '못 찾았다'고 보고하지 않는다
|
||||
_REPO_OBJECT = re.compile(r"^(Z?CL|Z?CX|Z?IF|Y?CL)_")
|
||||
|
||||
# `X-Y` 형태의 참조가 가리킬 수 있는 선언 종류. `TABLES: BSEG` 작업영역이나 타입풀은 여기 없다 —
|
||||
# `LIKE BSEG-WRBTR` 은 사전에서 타입을 빌려 쓴 것이라 그 선언을 함께 가져갈 필요가 없다.
|
||||
_COMPONENT_KINDS = {"data", "types", "constants", "ranges", "select-option", "field-symbol"}
|
||||
|
||||
# 호출문의 파라미터 구획 — 이 뒤의 `이름 = 값` 에서 왼쪽 이름은 호출 대상의 인터페이스다
|
||||
_SECTION_KEYWORDS = {"EXPORTING", "IMPORTING", "CHANGING", "TABLES", "RECEIVING", "EXCEPTIONS"}
|
||||
|
||||
|
||||
def _row(d: sqlite3.Row, via: str) -> dict:
|
||||
return {
|
||||
"name": d["name"], "kind": d["kind"], "scope": d["scope"], "include": d["include"],
|
||||
"line_start": d["line_start"], "line_end": d["line_end"], "code": d["code"],
|
||||
"type_text": d["type_text"] or "", "depends": loads(d["depends_json"]) or [], "via": via,
|
||||
}
|
||||
|
||||
|
||||
def _index(con: sqlite3.Connection, program: str) -> tuple[dict, dict]:
|
||||
"""(전역 선언 {이름: row}, unit 로컬 선언 {(unit_id, 이름): row})
|
||||
|
||||
같은 이름이 여러 번 선언돼 있으면(공용 인클루드 중복) 먼저 나온 것을 쓴다 — 결정론적이면 된다.
|
||||
"""
|
||||
glob: dict[str, sqlite3.Row] = {}
|
||||
local: dict[tuple[str, str], sqlite3.Row] = {}
|
||||
for d in con.execute(
|
||||
"SELECT * FROM declaration WHERE program=? ORDER BY include, line_start", (program,)
|
||||
):
|
||||
if d["scope"] == "unit" and d["unit_id"]:
|
||||
local.setdefault((d["unit_id"], d["name"]), d)
|
||||
else:
|
||||
glob.setdefault(d["name"], d)
|
||||
return glob, local
|
||||
|
||||
|
||||
def used_names(code: str) -> list[str]:
|
||||
"""코드에 등장하는 식별자 (등장 순서 유지).
|
||||
|
||||
보통은 기본 이름만 남기지만(`GT_LOAD-SEQNO` → GT_LOAD), **TYPE/LIKE 뒤의 참조는 컴포넌트를
|
||||
붙인 채로** 돌려준다(`LIKE BSEG-WRBTR` → BSEG-WRBTR). 사전 타입을 빌려 쓴 것과 작업영역을
|
||||
실제로 쓰는 것을 뒤에서 구분하기 위해서다.
|
||||
"""
|
||||
out: list[str] = []
|
||||
|
||||
def emit(ref: str) -> None:
|
||||
if base_name(ref) in BUILTIN_TYPES:
|
||||
return
|
||||
if (_FIELD_SYMBOL.match(base_name(ref)) or re.match(r"^[A-Z_/][A-Z0-9_/-]*$", ref)) \
|
||||
and ref not in out:
|
||||
out.append(ref)
|
||||
|
||||
# 문장 단위로 본다 — `CALL FUNCTION` 의 **형식 파라미터 이름**(I_BUKRS = gv_bukrs 의 왼쪽)과
|
||||
# 예외 이름(NO_RATE_FOUND)은 이 프로그램의 변수가 아니라 호출 대상의 인터페이스다.
|
||||
# 이름만 보고는 변수와 구분되지 않아(둘 다 I_/P_/NO_ …) 여기서 걸러야 한다.
|
||||
for st in split_statements(code, "chunk")[0]:
|
||||
up = st.upper
|
||||
# 파라미터 구획 키워드가 있으면 `name =` 의 왼쪽은 형식 파라미터다.
|
||||
# `CALL FUNCTION` 뿐 아니라 함수형 메서드 호출(`cl_x=>f( IMPORTING r = v )`)도 여기 걸린다.
|
||||
is_call = bool(up) and (up[0] == "CALL" or bool(_SECTION_KEYWORDS & set(up)))
|
||||
in_exceptions = False
|
||||
i = 0
|
||||
while i < len(up):
|
||||
t = up[i]
|
||||
if t in {"TYPE", "LIKE"}:
|
||||
name, i = type_name_after(up, i)
|
||||
if name:
|
||||
emit(clean_ref(name))
|
||||
continue
|
||||
if is_call and t == "EXCEPTIONS":
|
||||
in_exceptions = True
|
||||
elif in_exceptions or (is_call and i + 1 < len(up) and up[i + 1] == "="):
|
||||
pass # 예외 이름 / 형식 파라미터 이름 — 건너뛴다
|
||||
else:
|
||||
emit(base_name(t))
|
||||
i += 1
|
||||
return out
|
||||
|
||||
|
||||
def resolve(con: sqlite3.Connection, program: str, unit_id: str | None, code: str,
|
||||
self_include: str | None = None, self_range: tuple[int, int] | None = None) -> dict:
|
||||
"""코드 한 덩어리 → 정의부.
|
||||
|
||||
self_include/self_range 를 주면 **그 범위 안에 있는 선언은 뺀다** — 이미 코드에 들어 있으므로
|
||||
앞에 또 붙이면 중복 선언이 된다.
|
||||
"""
|
||||
glob, local = _index(con, program)
|
||||
params = {r["name"] for r in con.execute(
|
||||
"SELECT name FROM symbol WHERE program=? AND unit_id=? AND kind='param'", (program, unit_id))}
|
||||
|
||||
def lookup(name: str, scoped: bool) -> sqlite3.Row | None:
|
||||
if scoped and unit_id and (unit_id, name) in local:
|
||||
return local[(unit_id, name)]
|
||||
return glob.get(name)
|
||||
|
||||
def inside_self(d: sqlite3.Row) -> bool:
|
||||
return bool(self_range and self_include == d["include"]
|
||||
and d["line_start"] >= self_range[0] and d["line_end"] <= self_range[1])
|
||||
|
||||
picked: dict[tuple[str, str, int], dict] = {} # (include, name, line_start) → row
|
||||
external: list[str] = []
|
||||
unresolved: list[str] = []
|
||||
param_hits: list[str] = []
|
||||
ordered: list[dict] = []
|
||||
visiting: set[str] = set()
|
||||
|
||||
def walk(ref: str, via: str, scoped: bool) -> None:
|
||||
"""의존을 먼저 방문(post-order)해 TYPES 가 DATA 보다 앞에 오게 한다.
|
||||
|
||||
`ref` 는 `GT_DATA` 이거나 컴포넌트까지 붙은 `BKPF-BELNR` 이다. 후자가 DDIC 테이블을
|
||||
가리키면(= 지역 선언이 `TABLES:` 작업영역뿐이면) 사전 타입을 빌려 쓴 것이므로
|
||||
**작업영역 선언을 끌어오지 않는다** — 붙여넣는 코드에는 필요 없다.
|
||||
"""
|
||||
name = base_name(ref)
|
||||
if name in visiting:
|
||||
return
|
||||
d = lookup(name, scoped)
|
||||
if d is not None and "-" in ref and d["kind"] not in _COMPONENT_KINDS:
|
||||
if name not in external:
|
||||
external.append(name)
|
||||
return
|
||||
if d is None:
|
||||
if name in params:
|
||||
if name not in param_hits:
|
||||
param_hits.append(name)
|
||||
elif via == "dependency":
|
||||
if name not in external:
|
||||
external.append(name) # DDIC 타입/구조 — 선언을 가져갈 필요가 없다
|
||||
elif (_VARLIKE.match(name) or _FIELD_SYMBOL.match(name)) \
|
||||
and not _REPO_OBJECT.match(name) and name not in unresolved:
|
||||
unresolved.append(name) # 변수처럼 생겼는데 선언이 없다 (수집 누락 가능)
|
||||
return
|
||||
key = (d["include"], d["name"], d["line_start"])
|
||||
if key in picked or inside_self(d):
|
||||
return
|
||||
visiting.add(name)
|
||||
for dep in loads(d["depends_json"]) or []:
|
||||
walk(dep, "dependency", d["scope"] == "unit")
|
||||
visiting.discard(name)
|
||||
if key not in picked:
|
||||
row = _row(d, via)
|
||||
picked[key] = row
|
||||
ordered.append(row)
|
||||
|
||||
for n in used_names(code):
|
||||
walk(n, "used", True)
|
||||
|
||||
truncated = len(ordered) > MAX_DECLS
|
||||
ordered = ordered[:MAX_DECLS]
|
||||
return {
|
||||
"count": len(ordered),
|
||||
"declarations": ordered,
|
||||
"code": render(ordered, program),
|
||||
"external_refs": external,
|
||||
"params": param_hits,
|
||||
"unresolved": unresolved,
|
||||
"truncated": truncated,
|
||||
}
|
||||
|
||||
|
||||
def render(decls: list[dict], program: str = "") -> str:
|
||||
"""선언 목록 → 그대로 붙여넣을 수 있는 정의부 한 덩어리 (출처를 주석으로 남긴다)."""
|
||||
if not decls:
|
||||
return ""
|
||||
out = [
|
||||
"*&---------------------------------------------------------------------*",
|
||||
f"*& 정의부 — {program} 에서 가져온 선언. 이 로직을 옮겨 붙일 때 함께 필요하다.",
|
||||
"*& 이미 같은 선언이 있으면 버리고, 이름이 겹치면 바꿔서 쓸 것.",
|
||||
"*&---------------------------------------------------------------------*",
|
||||
]
|
||||
seen_span: set[tuple[str, int, int]] = set()
|
||||
n_lines = 0
|
||||
for d in decls:
|
||||
span = (d["include"], d["line_start"], d["line_end"])
|
||||
if span in seen_span: # 한 줄 체인(`TABLES: a, b.`)에서 온 형제들
|
||||
continue
|
||||
seen_span.add(span)
|
||||
body = d["code"]
|
||||
n_lines += body.count("\n") + 1
|
||||
if n_lines > MAX_CODE_LINES:
|
||||
out.append(f"* … (정의부가 길어 생략 — 나머지는 GET /programs/{program}/declarations)")
|
||||
break
|
||||
out.append(f"* {d['include']} L{d['line_start']}-L{d['line_end']} · {d['kind']}"
|
||||
f"{' · unit 로컬' if d['scope'] == 'unit' else ''}")
|
||||
out.append(body)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def for_chunk(con: sqlite3.Connection, chunk: sqlite3.Row, code: str) -> dict:
|
||||
"""logic_chunk 행 + 조각 코드 → 정의부."""
|
||||
return resolve(con, chunk["program"], chunk["unit_id"], code,
|
||||
self_include=chunk["include"],
|
||||
self_range=(chunk["line_start"], chunk["line_end"]))
|
||||
|
||||
|
||||
def program_declarations(con: sqlite3.Connection, program: str, scope: str | None = None) -> list[dict]:
|
||||
"""프로그램의 선언 전체 (위키 `## 정의부` 섹션용)."""
|
||||
sql = "SELECT * FROM declaration WHERE program=?"
|
||||
params: list = [program.upper()]
|
||||
if scope:
|
||||
sql += " AND scope=?"
|
||||
params.append(scope)
|
||||
return [_row(d, "catalog") for d in con.execute(sql + " ORDER BY include, line_start", params)]
|
||||
Reference in New Issue
Block a user