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