184 lines
5.8 KiB
Python
184 lines
5.8 KiB
Python
"""Semantic C++ source-file chunker.
|
|
|
|
Splits large C++ files at meaningful boundaries (class, struct, namespace,
|
|
free function definitions) so that each chunk can be migrated independently
|
|
by the LLM without losing context at arbitrary line positions.
|
|
|
|
Secondary fallback: nearest blank line below the 500-line mark.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import List
|
|
|
|
from migration_pipeline.models import CodeChunk
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def split_semantic(source: str, max_lines: int = 500) -> List[CodeChunk]:
|
|
"""Split *source* into chunks of at most *max_lines* lines.
|
|
|
|
Returns a list with a single CodeChunk (boundary_type="complete") when
|
|
the file is small enough to fit in one shot.
|
|
"""
|
|
lines = source.splitlines(keepends=True)
|
|
n = len(lines)
|
|
|
|
if n <= max_lines:
|
|
return [CodeChunk(0, 0, n, source, "complete")]
|
|
|
|
boundaries = _find_top_level_boundaries(lines)
|
|
return _partition(lines, boundaries, max_lines)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Boundary detection
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Matches the opening of a top-level class/struct/namespace/enum-class.
|
|
_COMPOSITE_RE = re.compile(
|
|
r"""^\s*
|
|
(?:template\s*<[^>]*>\s*)? # optional template<...>
|
|
(?:class|struct|namespace|enum\s+class)\s+\w
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
# Matches a free-function or method *definition* opening (has `{` at end of
|
|
# logical line, not just a forward declaration ending with `;`).
|
|
_FUNC_DEF_RE = re.compile(
|
|
r"""^ # start of line
|
|
[\w:<>\*&~\s]+ # return type / qualifier
|
|
\s+\w[\w:<>]* # function name
|
|
\s*\( # opening paren
|
|
""",
|
|
re.VERBOSE,
|
|
)
|
|
|
|
|
|
def _find_top_level_boundaries(lines: List[str]) -> List[int]:
|
|
"""Return line indices (0-based) that start a new top-level declaration."""
|
|
boundaries: List[int] = [0]
|
|
brace_depth = 0
|
|
in_block_comment = False
|
|
i = 0
|
|
|
|
while i < len(lines):
|
|
raw = lines[i]
|
|
line = raw.strip()
|
|
|
|
# ---- Block-comment tracking ----------------------------------------
|
|
if in_block_comment:
|
|
if "*/" in raw:
|
|
in_block_comment = False
|
|
i += 1
|
|
continue
|
|
|
|
if "/*" in raw and "*/" not in raw:
|
|
in_block_comment = True
|
|
|
|
# ---- Skip single-line comments, empty lines, preprocessor ----------
|
|
if not line or line.startswith("//") or line.startswith("#"):
|
|
i += 1
|
|
continue
|
|
|
|
# ---- At top level: look for new declarations -----------------------
|
|
if brace_depth == 0:
|
|
if _COMPOSITE_RE.match(raw) or _looks_like_func_def(lines, i):
|
|
if i > 0 and i not in boundaries:
|
|
boundaries.append(i)
|
|
|
|
# ---- Track brace depth (naïve, ignores strings/chars) -------------
|
|
# Remove string/char literals to avoid counting braces inside them.
|
|
cleaned = _strip_literals(raw)
|
|
brace_depth += cleaned.count("{") - cleaned.count("}")
|
|
brace_depth = max(0, brace_depth)
|
|
|
|
i += 1
|
|
|
|
return sorted(boundaries)
|
|
|
|
|
|
def _looks_like_func_def(lines: List[str], start: int) -> bool:
|
|
"""Check whether lines[start:] begins a function *definition* (has `{`)."""
|
|
# Collect up to 5 continuation lines for multi-line signatures.
|
|
snippet = ""
|
|
for j in range(start, min(start + 5, len(lines))):
|
|
snippet += lines[j]
|
|
if "{" in snippet:
|
|
return bool(_FUNC_DEF_RE.match(snippet.lstrip()))
|
|
if ";" in snippet:
|
|
return False # forward declaration
|
|
return False
|
|
|
|
|
|
_STRING_RE = re.compile(r'"[^"\\]*(?:\\.[^"\\]*)*"')
|
|
_CHAR_RE = re.compile(r"'[^'\\]*(?:\\.[^'\\]*)*'")
|
|
|
|
|
|
def _strip_literals(s: str) -> str:
|
|
s = _STRING_RE.sub('""', s)
|
|
s = _CHAR_RE.sub("''", s)
|
|
return s
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Partitioning
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _partition(
|
|
lines: List[str],
|
|
boundaries: List[int],
|
|
max_lines: int,
|
|
) -> List[CodeChunk]:
|
|
"""Greedily group lines into chunks that respect *boundaries* and *max_lines*."""
|
|
chunks: List[CodeChunk] = []
|
|
chunk_idx = 0
|
|
i = 0
|
|
n = len(lines)
|
|
boundary_set = set(boundaries)
|
|
|
|
while i < n:
|
|
target_end = min(i + max_lines, n)
|
|
|
|
# Prefer ending at the latest semantic boundary before target_end.
|
|
best_end = _best_boundary_before(boundaries, i + 1, target_end)
|
|
|
|
if best_end is None or best_end <= i:
|
|
# No semantic boundary: fall back to the nearest blank line.
|
|
best_end = _nearest_blank_before(lines, target_end, i)
|
|
|
|
content = "".join(lines[i:best_end])
|
|
boundary_type = "semantic" if best_end in boundary_set else "fallback"
|
|
if i == 0 and best_end == n:
|
|
boundary_type = "complete"
|
|
|
|
chunks.append(CodeChunk(chunk_idx, i, best_end, content, boundary_type))
|
|
chunk_idx += 1
|
|
i = best_end
|
|
|
|
return chunks
|
|
|
|
|
|
def _best_boundary_before(
|
|
boundaries: List[int], lo: int, hi: int
|
|
) -> int | None:
|
|
"""Return the largest boundary in (lo, hi], or None."""
|
|
best = None
|
|
for b in boundaries:
|
|
if lo < b <= hi:
|
|
best = b
|
|
return best
|
|
|
|
|
|
def _nearest_blank_before(lines: List[str], target: int, floor: int) -> int:
|
|
"""Search backwards from *target* for a blank line; return its index+1."""
|
|
for j in range(min(target, len(lines)) - 1, max(floor, target - 60), -1):
|
|
if not lines[j].strip():
|
|
return j + 1
|
|
return target # no blank found → hard cut
|