diff --git a/apps/backend/merge/file_merger.py b/apps/backend/merge/file_merger.py index 10380555..53cebc4d 100644 --- a/apps/backend/merge/file_merger.py +++ b/apps/backend/merge/file_merger.py @@ -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: diff --git a/apps/backend/merge/semantic_analysis/regex_analyzer.py b/apps/backend/merge/semantic_analysis/regex_analyzer.py index 40556f76..ae4eafa2 100644 --- a/apps/backend/merge/semantic_analysis/regex_analyzer.py +++ b/apps/backend/merge/semantic_analysis/regex_analyzer.py @@ -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( diff --git a/apps/backend/merge/semantic_analyzer.py b/apps/backend/merge/semantic_analyzer.py index 07aea590..67818d13 100644 --- a/apps/backend/merge/semantic_analyzer.py +++ b/apps/backend/merge/semantic_analyzer.py @@ -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)