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:
@@ -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
|
||||
Reference in New Issue
Block a user