fix(merge): handle Windows CRLF line endings in regex fallback

The merge conflict layer was failing on Windows when tree-sitter was
unavailable. The regex-based fallback used split("\n") which doesn't
handle CRLF line endings, and findall() returned tuples for JS/TS
patterns breaking function detection.

Changes:
- Normalize line endings (CRLF → LF) before parsing in regex_analyzer.py
- Use splitlines() instead of split("\n") in file_merger.py
- Fix tuple extraction from findall() for JS/TS function patterns
- Normalize line endings before tree-sitter parsing for consistent
  byte positions

All 111 merge tests pass. These changes are cross-platform safe and
maintain compatibility with macOS and Linux.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AndyMik90
2026-01-02 13:46:49 +01:00
parent 6d4231edca
commit 04de8c7848
3 changed files with 37 additions and 10 deletions
+4 -2
View File
@@ -45,7 +45,8 @@ def apply_single_task_changes(
# Addition - need to determine where to add
if change.change_type == ChangeType.ADD_IMPORT:
# Add import at top
lines = content.split("\n")
# Use splitlines() to handle all line ending styles (LF, CRLF, CR)
lines = content.splitlines()
import_end = find_import_end(lines, file_path)
lines.insert(import_end, change.content_after)
content = "\n".join(lines)
@@ -96,7 +97,8 @@ def combine_non_conflicting_changes(
# Add imports
if imports:
lines = content.split("\n")
# Use splitlines() to handle all line ending styles (LF, CRLF, CR)
lines = content.splitlines()
import_end = find_import_end(lines, file_path)
for imp in imports:
if imp.content_after and imp.content_after not in content:
@@ -30,11 +30,16 @@ def analyze_with_regex(
"""
changes: list[SemanticChange] = []
# Normalize line endings to LF for consistent cross-platform behavior
# This handles Windows CRLF, old Mac CR, and Unix LF
before_normalized = before.replace("\r\n", "\n").replace("\r", "\n")
after_normalized = after.replace("\r\n", "\n").replace("\r", "\n")
# Get a unified diff
diff = list(
difflib.unified_diff(
before.splitlines(keepends=True),
after.splitlines(keepends=True),
before_normalized.splitlines(keepends=True),
after_normalized.splitlines(keepends=True),
lineterm="",
)
)
@@ -89,8 +94,22 @@ def analyze_with_regex(
# Detect function changes (simplified)
func_pattern = get_function_pattern(ext)
if func_pattern:
funcs_before = set(func_pattern.findall(before))
funcs_after = set(func_pattern.findall(after))
# For JS/TS patterns with alternation, findall() returns tuples
# Extract the non-empty match from each tuple
def extract_func_names(matches):
names = set()
for match in matches:
if isinstance(match, tuple):
# Get the first non-empty group from the tuple
name = next((m for m in match if m), None)
if name:
names.add(name)
elif match:
names.add(match)
return names
funcs_before = extract_func_names(func_pattern.findall(before_normalized))
funcs_after = extract_func_names(func_pattern.findall(after_normalized))
for func in funcs_after - funcs_before:
changes.append(
+10 -4
View File
@@ -211,12 +211,18 @@ class SemanticAnalyzer:
"""Analyze using tree-sitter AST parsing."""
parser = self._parsers[ext]
tree_before = parser.parse(bytes(before, "utf-8"))
tree_after = parser.parse(bytes(after, "utf-8"))
# Normalize line endings to LF for consistent cross-platform behavior
# This ensures byte positions and line counts work correctly on all platforms
before_normalized = before.replace("\r\n", "\n").replace("\r", "\n")
after_normalized = after.replace("\r\n", "\n").replace("\r", "\n")
tree_before = parser.parse(bytes(before_normalized, "utf-8"))
tree_after = parser.parse(bytes(after_normalized, "utf-8"))
# Extract structural elements from both versions
elements_before = self._extract_elements(tree_before, before, ext)
elements_after = self._extract_elements(tree_after, after, ext)
# Use normalized content to match tree-sitter byte positions
elements_before = self._extract_elements(tree_before, before_normalized, ext)
elements_after = self._extract_elements(tree_after, after_normalized, ext)
# Compare and generate semantic changes
changes = compare_elements(elements_before, elements_after, ext)