"""ABAP 라인 → (코드, 주석) 분리와 토큰화. 목표는 완전한 ABAP 렉서가 아니라 (계획서 §11.3) 문장 패턴 매칭에 충분한 토큰열이다. - 줄 첫 문자 '*' → 전체 주석 줄 - 코드 중 '"' 부터 줄 끝 → 인라인 주석 (단, '...' / `...` / |...| 리터럴 내부 제외) - 식별자는 -, ->, =>, / 를 포함해 한 토큰으로 취급한다 (gs_head-belnr, zcl_x=>meth, lo_obj->meth, TEXT-004, /bic/xxx 등) - 필드심볼도 컴포넌트까지 한 토큰이다 (-fieldname). 이걸 쪼개면 일반 대입 판정(up[1] == "=")이 깨져 쓰기 지점이 누락되고 미인식 문장으로 잡힌다 — ALV 필드카탈로그를 채우는 관용구라 프로그램에 따라 미인식률을 12% 까지 끌어올렸다. """ from __future__ import annotations import re from dataclasses import dataclass, field @dataclass class Line: no: int # 1-based code: str # 주석 제거된 코드 부분 comment: str # 이 줄의 주석 텍스트 (전체주석/인라인 모두) is_full_comment: bool _TOKEN_RE = re.compile( r""" '(?:[^']|'')*' # '문자열' ('' 이스케이프) | `[^`]*` # `문자열` | \|[^|]*\| # |템플릿| | <[A-Za-z_][A-Za-z0-9_]*>(?:->|=>|[A-Za-z0-9_~/-])* # <필드심볼>[-컴포넌트 | ->메서드] | &[0-9]+ # 매크로(DEFINE) 치환 파라미터 &1 &2 … | [A-Za-z_/](?:->|=>|[A-Za-z0-9_~/-])*\(? # 식별자(-, ->, =>, /, ~ 포함; 단독 = 는 제외), 직결 여는괄호 허용 | [0-9]+ | ##[A-Za-z_]+ # pragma | . # 그 외 1문자 ( ) . , : = 등 """, re.VERBOSE, ) def split_comment(line: str) -> tuple[str, str, bool]: """한 줄을 (코드, 주석, 전체주석여부)로 분리.""" if line.startswith("*"): return "", line[1:].strip(), True code_chars: list[str] = [] i, n = 0, len(line) while i < n: ch = line[i] if ch == "'": j = i + 1 while j < n: if line[j] == "'": if j + 1 < n and line[j + 1] == "'": j += 2 continue break j += 1 code_chars.append(line[i : min(j + 1, n)]) i = j + 1 elif ch == "`": j = line.find("`", i + 1) j = n - 1 if j < 0 else j code_chars.append(line[i : j + 1]) i = j + 1 elif ch == "|": j = line.find("|", i + 1) j = n - 1 if j < 0 else j code_chars.append(line[i : j + 1]) i = j + 1 elif ch == '"': return "".join(code_chars), line[i + 1 :].strip(), False else: code_chars.append(ch) i += 1 return "".join(code_chars), "", False def tokenize_code(code: str) -> list[str]: tokens = [] for m in _TOKEN_RE.finditer(code): t = m.group(0) if t.strip(): # 식별자에 붙은 여는 괄호는 분리한다: "meth(" → "meth", "(" if len(t) > 1 and t.endswith("(") and not t.startswith(("'", "`", "|")): tokens.append(t[:-1]) tokens.append("(") else: tokens.append(t) return tokens def read_lines(text: str) -> list[Line]: out: list[Line] = [] for i, raw in enumerate(text.split("\n"), start=1): code, comment, full = split_comment(raw.rstrip("\r")) out.append(Line(no=i, code=code, comment=comment, is_full_comment=full)) return out