Skip to content

Commit 45cdc62

Browse files
authored
Merge pull request #944 from codeflash-ai/ashraf/cf-894-vsc-init-no-way-to-create-tests-dir-when-no-test-dir-present
lsp tests validation
2 parents 890243c + 491b3c1 commit 45cdc62

File tree

3 files changed

+144
-29
lines changed

3 files changed

+144
-29
lines changed

codeflash/cli_cmds/cmd_init.py

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from codeflash.cli_cmds.cli_common import apologize_and_exit
2626
from codeflash.cli_cmds.console import console, logger
2727
from codeflash.cli_cmds.extension import install_vscode_extension
28+
from codeflash.code_utils.code_utils import validate_relative_directory_path
2829
from codeflash.code_utils.compat import LF
2930
from codeflash.code_utils.config_parser import parse_config_file
3031
from codeflash.code_utils.env_utils import check_formatter_installed, get_codeflash_api_key
@@ -349,20 +350,32 @@ def collect_setup_info() -> CLISetupInfo:
349350
console.print(custom_panel)
350351
console.print()
351352

352-
custom_questions = [
353-
inquirer.Path(
354-
"custom_path",
355-
message="Enter the path to your module directory",
356-
path_type=inquirer.Path.DIRECTORY,
357-
exists=True,
358-
)
359-
]
353+
# Retry loop for custom module root path
354+
module_root = None
355+
while module_root is None:
356+
custom_questions = [
357+
inquirer.Path(
358+
"custom_path",
359+
message="Enter the path to your module directory",
360+
path_type=inquirer.Path.DIRECTORY,
361+
exists=True,
362+
)
363+
]
360364

361-
custom_answers = inquirer.prompt(custom_questions, theme=CodeflashTheme())
362-
if custom_answers:
363-
module_root = Path(custom_answers["custom_path"])
364-
else:
365-
apologize_and_exit()
365+
custom_answers = inquirer.prompt(custom_questions, theme=CodeflashTheme())
366+
if not custom_answers:
367+
apologize_and_exit()
368+
return None # unreachable but satisfies type checker
369+
370+
custom_path_str = str(custom_answers["custom_path"])
371+
# Validate the path is safe
372+
is_valid, error_msg = validate_relative_directory_path(custom_path_str)
373+
if not is_valid:
374+
click.echo(f"❌ Invalid path: {error_msg}")
375+
click.echo("Please enter a valid relative directory path.")
376+
console.print() # Add spacing before retry
377+
continue # Retry the prompt
378+
module_root = Path(custom_path_str)
366379
else:
367380
module_root = module_root_answer
368381
ph("cli-project-root-provided")
@@ -420,20 +433,32 @@ def collect_setup_info() -> CLISetupInfo:
420433
console.print(custom_tests_panel)
421434
console.print()
422435

423-
custom_tests_questions = [
424-
inquirer.Path(
425-
"custom_tests_path",
426-
message="Enter the path to your tests directory",
427-
path_type=inquirer.Path.DIRECTORY,
428-
exists=True,
429-
)
430-
]
436+
# Retry loop for custom tests root path
437+
tests_root = None
438+
while tests_root is None:
439+
custom_tests_questions = [
440+
inquirer.Path(
441+
"custom_tests_path",
442+
message="Enter the path to your tests directory",
443+
path_type=inquirer.Path.DIRECTORY,
444+
exists=True,
445+
)
446+
]
431447

432-
custom_tests_answers = inquirer.prompt(custom_tests_questions, theme=CodeflashTheme())
433-
if custom_tests_answers:
434-
tests_root = Path(curdir) / Path(custom_tests_answers["custom_tests_path"])
435-
else:
436-
apologize_and_exit()
448+
custom_tests_answers = inquirer.prompt(custom_tests_questions, theme=CodeflashTheme())
449+
if not custom_tests_answers:
450+
apologize_and_exit()
451+
return None # unreachable but satisfies type checker
452+
453+
custom_tests_path_str = str(custom_tests_answers["custom_tests_path"])
454+
# Validate the path is safe
455+
is_valid, error_msg = validate_relative_directory_path(custom_tests_path_str)
456+
if not is_valid:
457+
click.echo(f"❌ Invalid path: {error_msg}")
458+
click.echo("Please enter a valid relative directory path.")
459+
console.print() # Add spacing before retry
460+
continue # Retry the prompt
461+
tests_root = Path(curdir) / Path(custom_tests_path_str)
437462
else:
438463
tests_root = Path(curdir) / Path(cast("str", tests_root_answer))
439464

codeflash/code_utils/code_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
from codeflash.code_utils.config_parser import find_pyproject_toml, get_all_closest_config_files
2020
from codeflash.lsp.helpers import is_LSP_enabled
2121

22+
_INVALID_CHARS_NT = {"<", ">", ":", '"', "|", "?", "*"}
23+
24+
_INVALID_CHARS_UNIX = {"\0"}
25+
2226
ImportErrorPattern = re.compile(r"ModuleNotFoundError.*$", re.MULTILINE)
2327

2428
BLACKLIST_ADDOPTS = ("--benchmark", "--sugar", "--codespeed", "--cov", "--profile", "--junitxml", "-n")
@@ -376,3 +380,51 @@ def extract_unique_errors(pytest_output: str) -> set[str]:
376380
unique_errors.add(error_message)
377381

378382
return unique_errors
383+
384+
385+
def validate_relative_directory_path(path: str) -> tuple[bool, str]:
386+
"""Validate that a path is a safe relative directory path.
387+
388+
Prevents path traversal attacks and invalid paths.
389+
Works cross-platform (Windows, Linux, macOS).
390+
391+
Args:
392+
path: The path string to validate
393+
394+
Returns:
395+
tuple[bool, str]: (is_valid, error_message)
396+
- is_valid: True if path is valid, False otherwise
397+
- error_message: Empty string if valid, error description if invalid
398+
399+
"""
400+
if not path or not path.strip():
401+
return False, "Path cannot be empty"
402+
403+
# Normalize whitespace
404+
path = path.strip()
405+
406+
# Check for path traversal attempts (cross-platform)
407+
# Normalize path separators for checking
408+
normalized = path.replace("\\", "/")
409+
if ".." in normalized:
410+
return False, "Path cannot contain '..'. Use a relative path like 'tests' or 'src/app' instead"
411+
412+
# Check for absolute paths, invalid characters, and validate path format
413+
error_msg = ""
414+
if Path(path).is_absolute():
415+
error_msg = "Path must be relative, not absolute"
416+
elif os.name == "nt": # Windows
417+
if any(char in _INVALID_CHARS_NT for char in path):
418+
error_msg = "Path contains invalid characters for this operating system"
419+
elif "\0" in path: # Unix-like
420+
error_msg = "Path contains invalid characters for this operating system"
421+
else:
422+
# Validate using pathlib to ensure it's a valid path structure
423+
try:
424+
Path(path)
425+
except (ValueError, OSError) as e:
426+
error_msg = f"Invalid path format: {e!s}"
427+
428+
if error_msg:
429+
return False, error_msg
430+
return True, ""

codeflash/lsp/beta.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
get_valid_subdirs,
2424
is_valid_pyproject_toml,
2525
)
26+
from codeflash.code_utils.code_utils import validate_relative_directory_path
2627
from codeflash.code_utils.git_utils import git_root_dir
2728
from codeflash.code_utils.git_worktree_utils import create_worktree_snapshot_commit
2829
from codeflash.code_utils.shell_utils import save_api_key_to_rc
@@ -184,10 +185,47 @@ def write_config(params: WriteConfigParams) -> dict[str, any]:
184185
# the client provided a config path but it doesn't exist
185186
create_empty_pyproject_toml(cfg_file)
186187

188+
# Handle both dict and object access for config
189+
def get_config_value(key: str, default: str = "") -> str:
190+
if isinstance(cfg, dict):
191+
return cfg.get(key, default)
192+
return getattr(cfg, key, default)
193+
194+
tests_root = get_config_value("tests_root", "")
195+
# Validate tests_root path format and safety
196+
if tests_root:
197+
is_valid, error_msg = validate_relative_directory_path(tests_root)
198+
if not is_valid:
199+
return {
200+
"status": "error",
201+
"message": f"Invalid 'tests_root': {error_msg}",
202+
"field_errors": {"tests_root": error_msg},
203+
}
204+
# Validate tests_root directory exists if provided
205+
base_dir = cfg_file.parent if cfg_file else Path.cwd()
206+
tests_root_path = (base_dir / tests_root).resolve()
207+
if not tests_root_path.exists() or not tests_root_path.is_dir():
208+
return {
209+
"status": "error",
210+
"message": f"Invalid 'tests_root': directory does not exist at {tests_root_path}",
211+
"field_errors": {"tests_root": f"Directory does not exist at {tests_root_path}"},
212+
}
213+
214+
# Validate module_root path format and safety
215+
module_root = get_config_value("module_root", "")
216+
if module_root:
217+
is_valid, error_msg = validate_relative_directory_path(module_root)
218+
if not is_valid:
219+
return {
220+
"status": "error",
221+
"message": f"Invalid 'module_root': {error_msg}",
222+
"field_errors": {"module_root": error_msg},
223+
}
224+
187225
setup_info = VsCodeSetupInfo(
188-
module_root=getattr(cfg, "module_root", ""),
189-
tests_root=getattr(cfg, "tests_root", ""),
190-
formatter=get_formatter_cmds(getattr(cfg, "formatter_cmds", "disabled")),
226+
module_root=module_root,
227+
tests_root=tests_root,
228+
formatter=get_formatter_cmds(get_config_value("formatter_cmds", "disabled")),
191229
)
192230

193231
devnull_writer = open(os.devnull, "w") # noqa

0 commit comments

Comments
 (0)