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:
byeongwook.choi
2026-09-21 13:23:37 +09:00
co-authored by Claude Fable 5.1
commit 11ae3629b2
453 changed files with 259183 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
"""문장 분리 — '.' 종결, 체인(:` `,`) 전개. (계획서 §4.1)"""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from .tokenizer import Line, read_lines, tokenize_code
_NAME_HEAD = re.compile(r"^[<A-Za-z_/]")
def assignment_eq_index(up: list[str]) -> int | None:
"""평문 대입이면 최상위 '=' 토큰의 위치, 아니면 None.
받아들이는 형태 — 쓰기 지점 추출(dataflow)과 미인식 리포트(run)가 같은 판정을 써야 하므로
여기 한 곳에만 둔다:
X = ... → 1
X[] = ... → 3 (내부테이블 본문 대입 — 흔한 관용구)
X[ key = v ] = ... → 대괄호를 건너뛴 위치 (테이블 표현식)
X(3) = ... → 괄호를 건너뛴 위치 (오프셋·길이 쓰기)
X+4(2) = ... → 위와 같음
"""
if len(up) < 3 or not _NAME_HEAD.match(up[0]):
return None
i = 1
# 오프셋 표기 X+4(2) — '+'와 숫자를 먼저 건너뛴다
if i + 1 < len(up) and up[i] == "+" and up[i + 1].isdigit():
i += 2
for opener, closer in (("[", "]"), ("(", ")")):
if i < len(up) and up[i] == opener:
depth = 0
while i < len(up):
if up[i] == opener:
depth += 1
elif up[i] == closer:
depth -= 1
if depth == 0:
i += 1
break
i += 1
return i if i < len(up) and up[i] == "=" else None
@dataclass
class Statement:
include: str
line_start: int
line_end: int
tokens: list[str] # 원문 케이스
comment_text: str = "" # 같은 줄 / 직전 주석
raw_text: str = ""
# --- 체인(`DATA: a, b, c.`) 항목의 원문 복원용 (parser/declarations.py) ---
chain_head: str = "" # ':' 앞 토큰들 ("DATA") — 체인에서 전개된 항목만
chain_start: int = 0 # 체인 전체의 시작 줄
chain_end: int = 0 # 체인 전체의 끝 줄('.' 이 있는 줄)
@property
def upper(self) -> list[str]:
return [t.upper() for t in self.tokens]
def text(self) -> str:
return " ".join(self.tokens)
def _expand_chain(tokens: list[str], token_lines: list[int]) -> list[tuple[list[str], int, int]]:
"""'DATA: a TYPE i, b TYPE i' → [(['DATA','a','TYPE','i'], 줄, 줄), (['DATA','b',...], 줄, 줄)]
괄호 안의 ','는 분리하지 않는다. 항목마다 **자기 토큰이 놓인 줄 범위**를 함께 돌려준다 —
체인 항목의 선언 원문을 그 항목만큼만 잘라내려면(정의부 수집) 항목별 줄이 있어야 한다.
체인이 아니면 빈 목록을 돌려준다(호출 측이 문장 전체 범위를 쓴다).
"""
if ":" not in tokens:
return []
ci = tokens.index(":")
head, head_lines = tokens[:ci], token_lines[:ci]
rest, rest_lines = tokens[ci + 1 :], token_lines[ci + 1 :]
groups: list[tuple[list[str], list[int]]] = []
cur: list[str] = []
cur_lines: list[int] = []
depth = 0
for t, ln in zip(rest, rest_lines):
if t == "(":
depth += 1
elif t == ")":
depth = max(0, depth - 1)
if t == "," and depth == 0:
groups.append((cur, cur_lines))
cur, cur_lines = [], []
else:
cur.append(t)
cur_lines.append(ln)
groups.append((cur, cur_lines))
out: list[tuple[list[str], int, int]] = []
fallback = head_lines[0] if head_lines else (token_lines[0] if token_lines else 0)
for g, gl in groups:
if not (head or g):
continue
start = gl[0] if gl else fallback
end = gl[-1] if gl else start
out.append((head + g, start, end))
return out
def split_statements(text: str, include: str) -> tuple[list[Statement], list[Line]]:
"""인클루드 소스 → 문장 목록. 주석 줄은 직후 문장의 comment_text 로 전달."""
lines = read_lines(text)
statements: list[Statement] = []
pending_tokens: list[str] = []
pending_lines: list[int] = [] # pending_tokens 와 같은 길이 — 토큰이 놓인 줄
pending_start: int | None = None
pending_comments: list[str] = []
inline_comments: list[str] = []
def flush(end_line: int) -> None:
nonlocal pending_tokens, pending_lines, pending_start, pending_comments, inline_comments
if pending_tokens:
comment = " ".join(c for c in (pending_comments + inline_comments) if c).strip()
start = pending_start or end_line
items = _expand_chain(pending_tokens, pending_lines)
if not items: # 체인이 아니면 문장 = 전체 범위
items = [(pending_tokens, start, end_line)]
chain_head = ""
else:
chain_head = " ".join(pending_tokens[: pending_tokens.index(":")])
for i, (toks, ls, le) in enumerate(items):
if not toks:
continue
statements.append(
Statement(
include=include,
line_start=ls,
# 마지막 항목은 종결 '.' 이 있는 줄까지 — 원문을 잘라도 문장이 닫힌다
line_end=end_line if i == len(items) - 1 else le,
tokens=toks,
comment_text=comment[:500],
chain_head=chain_head,
chain_start=start,
chain_end=end_line,
)
)
pending_tokens = []
pending_lines = []
pending_start = None
pending_comments = []
inline_comments = []
for ln in lines:
if ln.is_full_comment:
if pending_tokens:
inline_comments.append(ln.comment)
else:
pending_comments.append(ln.comment)
continue
if ln.comment:
inline_comments.append(ln.comment)
toks = tokenize_code(ln.code)
if not toks:
if not pending_tokens:
# 빈 줄이 이어지면 이전 주석은 다음 문장과 무관해질 수 있으나, 그대로 유지(요약 컨텍스트용)
pass
continue
for t in toks:
if t == ".":
flush(ln.no)
else:
if pending_start is None:
pending_start = ln.no
pending_tokens.append(t)
pending_lines.append(ln.no)
flush(len(lines))
for st in statements:
st.raw_text = st.text()[:400]
return statements, lines