diff --git a/codeflash/discovery/functions_to_optimize.py b/codeflash/discovery/functions_to_optimize.py index b8cf895e1..f29cf1325 100644 --- a/codeflash/discovery/functions_to_optimize.py +++ b/codeflash/discovery/functions_to_optimize.py @@ -1,7 +1,6 @@ from __future__ import annotations import ast -import os import random import warnings from _ast import AsyncFunctionDef, ClassDef, FunctionDef @@ -669,30 +668,74 @@ def filter_functions( submodule_ignored_paths_count: int = 0 blocklist_funcs_removed_count: int = 0 previous_checkpoint_functions_removed_count: int = 0 - tests_root_str = str(tests_root) - module_root_str = str(module_root) + + def _resolve_path(path: Path | str) -> Path: + # Use strict=False so we don't fail on paths that don't exist yet (e.g. worktree paths) + return Path(path).resolve(strict=False) + + # Resolve all root paths to absolute paths for consistent comparison + tests_root_resolved = _resolve_path(tests_root) + module_root_resolved = _resolve_path(module_root) + + # Resolve ignore paths and submodule paths + ignore_paths_resolved = [_resolve_path(p) for p in ignore_paths] + submodule_paths_resolved = [_resolve_path(p) for p in submodule_paths] # We desperately need Python 3.10+ only support to make this code readable with structural pattern matching for file_path_path, functions in modified_functions.items(): _functions = functions - file_path = str(file_path_path) - if file_path.startswith(tests_root_str + os.sep): + # Resolve file path to absolute path + # Convert to Path if it's a string (e.g., from get_functions_within_git_diff) + file_path_obj = _resolve_path(file_path_path) + try: + file_path_resolved = file_path_obj.resolve() + except (OSError, RuntimeError): + file_path_resolved = file_path_obj.absolute() if not file_path_obj.is_absolute() else file_path_obj + + file_path = str(file_path_obj) + + # Check if file is in tests root using resolved paths + try: + file_path_resolved.relative_to(tests_root_resolved) test_functions_removed_count += len(_functions) continue - if file_path in ignore_paths or any( - file_path.startswith(str(ignore_path) + os.sep) for ignore_path in ignore_paths - ): + except ValueError: + pass # File is not in tests root, continue checking + + # Check if file is in ignore paths using resolved paths + is_ignored = False + for ignore_path_resolved in ignore_paths_resolved: + try: + file_path_resolved.relative_to(ignore_path_resolved) + is_ignored = True + break + except ValueError: + pass + if is_ignored: ignore_paths_removed_count += 1 continue - if file_path in submodule_paths or any( - file_path.startswith(str(submodule_path) + os.sep) for submodule_path in submodule_paths - ): + + # Check if file is in submodule paths using resolved paths + is_in_submodule = False + for submodule_path_resolved in submodule_paths_resolved: + try: + file_path_resolved.relative_to(submodule_path_resolved) + is_in_submodule = True + break + except ValueError: + pass + if is_in_submodule: submodule_ignored_paths_count += 1 continue - if path_belongs_to_site_packages(Path(file_path)): + + if path_belongs_to_site_packages(file_path_resolved): site_packages_removed_count += len(_functions) continue - if not file_path.startswith(module_root_str + os.sep): + + # Check if file is in module root using resolved paths + try: + file_path_resolved.relative_to(module_root_resolved) + except ValueError: non_modules_removed_count += len(_functions) continue try: diff --git a/codeflash/lsp/beta.py b/codeflash/lsp/beta.py index 14d43a1a5..8135333e5 100644 --- a/codeflash/lsp/beta.py +++ b/codeflash/lsp/beta.py @@ -479,10 +479,13 @@ def initialize_function_optimization(params: FunctionOptimizationInitParams) -> server.current_optimization_init_result = initialization_result.unwrap() server.show_message_log(f"Successfully initialized optimization for {params.functionName}", "Info") - files = [document.path] + # Use the worktree file path (which is normalized) instead of document.path + # document.path might be malformed on Windows (missing drive letter) + worktree_file_path = str(server.optimizer.args.file.resolve()) + files = [worktree_file_path] _, _, original_helpers = server.current_optimization_init_result - files.extend([str(helper_path) for helper_path in original_helpers]) + files.extend([str(helper_path.resolve()) for helper_path in original_helpers]) return {"functionName": params.functionName, "status": "success", "files_inside_context": files} diff --git a/codeflash/optimization/optimizer.py b/codeflash/optimization/optimizer.py index ddbb894d1..0f88b66ec 100644 --- a/codeflash/optimization/optimizer.py +++ b/codeflash/optimization/optimizer.py @@ -485,7 +485,49 @@ def mirror_paths_for_worktree_mode(self, worktree_dir: Path) -> None: def mirror_path(path: Path, src_root: Path, dest_root: Path) -> Path: - relative_path = path.relative_to(src_root) + # Resolve both paths to absolute paths to handle Windows path normalization issues + # This ensures paths with/without drive letters are handled correctly + try: + path_resolved = path.resolve() + except (OSError, RuntimeError): + # If resolve fails (e.g., path doesn't exist or is malformed), try absolute() + path_resolved = path.absolute() if not path.is_absolute() else path + + try: + src_root_resolved = src_root.resolve() + except (OSError, RuntimeError): + src_root_resolved = src_root.absolute() if not src_root.is_absolute() else src_root + + # Normalize paths using os.path.normpath and normcase for cross-platform compatibility + path_str = os.path.normpath(str(path_resolved)) + src_root_str = os.path.normpath(str(src_root_resolved)) + + # Convert to lowercase for case-insensitive comparison on Windows + path_normalized = os.path.normcase(path_str) + src_root_normalized = os.path.normcase(src_root_str) + + # Try using Path.relative_to with resolved paths first + try: + relative_path = path_resolved.relative_to(src_root_resolved) + except ValueError as err: + # If relative_to fails, manually extract the relative path using normalized strings + if path_normalized.startswith(src_root_normalized): + # Extract relative path manually + # Use the original path_str to preserve proper path format + if path_str.startswith(src_root_str): + relative_str = path_str[len(src_root_str) :].lstrip(os.sep) + relative_path = Path(relative_str) + else: + # Fallback: use normalized paths + relative_str = path_normalized[len(src_root_normalized) :].lstrip(os.sep) + relative_path = Path(relative_str) + else: + error_msg = ( + f"Path {path_resolved} (normalized: {path_normalized}) is not relative to " + f"{src_root_resolved} (normalized: {src_root_normalized})" + ) + raise ValueError(error_msg) from err + return dest_root / relative_path diff --git a/tests/test_function_discovery.py b/tests/test_function_discovery.py index 49a67ba9b..76b3445a1 100644 --- a/tests/test_function_discovery.py +++ b/tests/test_function_discovery.py @@ -411,13 +411,17 @@ def not_in_checkpoint_function(): discovered = find_all_functions_in_file(test_file_path) modified_functions = {test_file_path: discovered[test_file_path]} - filtered, count = filter_functions( - modified_functions, - tests_root=Path("tests"), - ignore_paths=[], - project_root=temp_dir, - module_root=temp_dir, - ) + # Use an absolute path for tests_root that won't match the temp directory + # This avoids path resolution issues in CI where the working directory might differ + tests_root_absolute = (temp_dir.parent / "nonexistent_tests_dir").resolve() + with unittest.mock.patch("codeflash.discovery.functions_to_optimize.get_blocklisted_functions", return_value={}): + filtered, count = filter_functions( + modified_functions, + tests_root=tests_root_absolute, + ignore_paths=[], + project_root=temp_dir, + module_root=temp_dir, + ) function_names = [fn.function_name for fn in filtered.get(test_file_path, [])] assert "propagate_attributes" in function_names assert count == 3