Skip to content

Commit 823300f

Browse files
authored
Merge branch 'main' into java-config-redesign
2 parents 6d65dd5 + c077997 commit 823300f

13 files changed

Lines changed: 395 additions & 73 deletions

File tree

codeflash/languages/base.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ class IndexResult:
5050
error: bool
5151

5252

53+
@dataclass(frozen=True)
54+
class SetupError:
55+
message: str
56+
should_abort: bool
57+
58+
5359
@dataclass
5460
class HelperFunction:
5561
"""A helper function that is a dependency of the target function.
@@ -725,8 +731,12 @@ def prepare_module(
725731
"""Parse/validate a module before optimization."""
726732
...
727733

728-
def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> None:
729-
"""One-time project setup after language detection. Default: no-op."""
734+
def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> bool:
735+
"""One-time project setup after language detection. Default: no-op.
736+
737+
Returns True if the project is valid for optimization, False otherwise.
738+
"""
739+
return True
730740

731741
def adjust_test_config_for_discovery(self, test_cfg: TestConfig) -> None:
732742
"""Adjust test config before test discovery. Default: no-op."""

codeflash/languages/java/build_tool_strategy.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import logging
1111
import os
12+
import shutil
1213
from abc import ABC, abstractmethod
1314
from pathlib import Path
1415
from typing import TYPE_CHECKING, Any
@@ -73,6 +74,18 @@ def find_runtime_jar(self) -> Path | None:
7374

7475
return None
7576

77+
def find_wrapper_executable(
78+
self, build_root: Path, wrapper_names: tuple[str, ...], system_command: str
79+
) -> str | None:
80+
search = build_root.resolve()
81+
while search != search.parent:
82+
for name in wrapper_names:
83+
candidate = search / name
84+
if candidate.exists():
85+
return str(candidate)
86+
search = search.parent
87+
return shutil.which(system_command)
88+
7689
@abstractmethod
7790
def find_executable(self, build_root: Path) -> str | None:
7891
"""Find the build tool executable, searching up parent directories if needed."""

codeflash/languages/java/gradle_strategy.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -417,22 +417,7 @@ def get_project_info(self, project_root: Path) -> JavaProjectInfo | None:
417417
)
418418

419419
def find_executable(self, build_root: Path) -> str | None:
420-
# Walk up from build_root to find gradlew — for multi-module projects
421-
# the wrapper lives at the repo root, which may be a parent of build_root.
422-
current = build_root.resolve()
423-
while True:
424-
gradlew_path = current / "gradlew"
425-
if gradlew_path.exists():
426-
return str(gradlew_path)
427-
gradlew_bat_path = current / "gradlew.bat"
428-
if gradlew_bat_path.exists():
429-
return str(gradlew_bat_path)
430-
parent = current.parent
431-
if parent == current:
432-
break
433-
current = parent
434-
# Fall back to system Gradle
435-
return shutil.which("gradle")
420+
return self.find_wrapper_executable(build_root, ("gradlew", "gradlew.bat"), "gradle")
436421

437422
def ensure_runtime(self, build_root: Path, test_module: str | None) -> bool:
438423
runtime_jar = self.find_runtime_jar()

codeflash/languages/java/maven_strategy.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -647,21 +647,7 @@ def get_text(xpath: str, default: str | None = None) -> str | None:
647647
return None
648648

649649
def find_executable(self, build_root: Path) -> str | None:
650-
# Walk up parent directories to find mvnw (multi-module projects keep it in the root)
651-
search = build_root.resolve()
652-
while search != search.parent:
653-
mvnw_path = search / "mvnw"
654-
if mvnw_path.exists():
655-
return str(mvnw_path)
656-
mvnw_cmd_path = search / "mvnw.cmd"
657-
if mvnw_cmd_path.exists():
658-
return str(mvnw_cmd_path)
659-
search = search.parent
660-
if Path("mvnw").exists():
661-
return "./mvnw"
662-
if Path("mvnw.cmd").exists():
663-
return "mvnw.cmd"
664-
return shutil.which("mvn")
650+
return self.find_wrapper_executable(build_root, ("mvnw", "mvnw.cmd"), "mvn")
665651

666652
def find_runtime_jar(self) -> Path | None:
667653
if self._M2_JAR.exists():

codeflash/languages/java/support.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,12 @@ def load_coverage(
403403
) -> None:
404404
return None
405405

406-
def setup_test_config(self, test_cfg: Any, file_path: Path, current_worktree: Path | None = None) -> None:
406+
def setup_test_config(self, test_cfg: Any, file_path: Path, current_worktree: Path | None = None) -> bool:
407407
"""Detect test framework from project build config (pom.xml / build.gradle)."""
408408
config = detect_java_project(test_cfg.project_root_path)
409409
if config is not None:
410410
self._test_framework = config.test_framework
411+
return True
411412

412413
def adjust_test_config_for_discovery(self, test_cfg: Any) -> None:
413414
"""Adjust test config before test discovery for Java.

codeflash/languages/javascript/optimizer.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import TYPE_CHECKING
44

55
from codeflash.cli_cmds.console import logger
6+
from codeflash.languages.base import SetupError
67
from codeflash.models.models import ValidCode
78

89
if TYPE_CHECKING:
@@ -25,19 +26,21 @@ def prepare_javascript_module(
2526
return validated_original_code, None
2627

2728

28-
def verify_js_requirements(test_cfg: TestConfig) -> None:
29+
def verify_js_requirements(test_cfg: TestConfig) -> list[SetupError]:
2930
"""Verify JavaScript/TypeScript requirements before optimization.
3031
3132
Checks that Node.js, npm, and the test framework are available.
3233
Logs warnings if requirements are not met but does not abort.
34+
35+
Returns: List of setup errors if requirements are not met, empty list otherwise.
3336
"""
3437
from codeflash.languages import get_language_support
3538
from codeflash.languages.base import Language
3639
from codeflash.languages.test_framework import get_js_test_framework_or_default
3740

3841
js_project_root = test_cfg.js_project_root
3942
if not js_project_root:
40-
return
43+
return [SetupError("JavaScript project root not found", should_abort=True)]
4144

4245
try:
4346
js_support = get_language_support(Language.JAVASCRIPT)
@@ -47,6 +50,9 @@ def verify_js_requirements(test_cfg: TestConfig) -> None:
4750
if not success:
4851
logger.warning("JavaScript requirements check found issues:")
4952
for error in errors:
50-
logger.warning(f" - {error}")
53+
logger.warning(f" - {error.message}")
54+
return errors
55+
return []
5156
except Exception as e:
5257
logger.debug(f"Failed to verify JS requirements: {e}")
58+
return [SetupError(str(e), should_abort=True)]

codeflash/languages/javascript/support.py

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@
1414

1515
from codeflash.code_utils.git_utils import git_root_dir, mirror_path
1616
from codeflash.discovery.functions_to_optimize import FunctionToOptimize
17-
from codeflash.languages.base import CodeContext, FunctionFilterCriteria, HelperFunction, Language, TestInfo, TestResult
17+
from codeflash.languages.base import (
18+
CodeContext,
19+
FunctionFilterCriteria,
20+
HelperFunction,
21+
Language,
22+
SetupError,
23+
TestInfo,
24+
TestResult,
25+
)
1826
from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage, get_analyzer_for_file
1927
from codeflash.languages.registry import register_language
2028
from codeflash.models.models import FunctionParent
@@ -1950,11 +1958,13 @@ def prepare_module(
19501958

19511959
return prepare_javascript_module(module_code, module_path)
19521960

1953-
def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None) -> None:
1961+
def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None) -> bool:
19541962
from codeflash.languages.javascript.optimizer import verify_js_requirements
19551963
from codeflash.languages.javascript.test_runner import find_node_project_root
19561964

19571965
test_cfg.js_project_root = find_node_project_root(file_path)
1966+
if test_cfg.js_project_root is None:
1967+
return False
19581968
if current_worktree is not None:
19591969
original_js_root = git_root_dir()
19601970
worktree_node_modules = test_cfg.js_project_root / "node_modules"
@@ -1970,7 +1980,11 @@ def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_workt
19701980
original_root_node_modules = original_js_root / "node_modules"
19711981
if original_root_node_modules.exists() and not worktree_root_node_modules.exists():
19721982
worktree_root_node_modules.symlink_to(original_root_node_modules)
1973-
verify_js_requirements(test_cfg)
1983+
setup_errors = verify_js_requirements(test_cfg)
1984+
if any(e.should_abort for e in setup_errors):
1985+
return False
1986+
1987+
return True
19741988

19751989
def adjust_test_config_for_discovery(self, test_cfg: TestConfig) -> None:
19761990
test_cfg.tests_project_rootdir = test_cfg.tests_root
@@ -2164,8 +2178,9 @@ def find_test_root(self, project_root: Path) -> Path | None:
21642178
def get_module_path(self, source_file: Path, project_root: Path, tests_root: Path | None = None) -> str:
21652179
"""Get the module path for importing a JavaScript source file from tests.
21662180
2167-
For JavaScript, this returns a relative path from the tests directory to the source file
2168-
(e.g., '../fibonacci' for source at /project/fibonacci.js and tests at /project/tests/).
2181+
For JavaScript/TypeScript, this returns a relative path from the tests directory to
2182+
the source file. For ESM projects or TypeScript, the path includes a .js extension
2183+
(TypeScript convention). For CommonJS, no extension is added.
21692184
21702185
Args:
21712186
source_file: Path to the source file.
@@ -2179,13 +2194,15 @@ def get_module_path(self, source_file: Path, project_root: Path, tests_root: Pat
21792194
import os
21802195

21812196
from codeflash.cli_cmds.console import logger
2197+
from codeflash.languages.javascript.module_system import ModuleSystem, detect_module_system
21822198

21832199
if tests_root is None:
21842200
tests_root = self.find_test_root(project_root) or project_root
21852201

21862202
try:
21872203
# Resolve both paths to absolute to ensure consistent relative path calculation
2188-
source_file_abs = source_file.resolve().with_suffix("")
2204+
# Note: Don't remove extension yet - we'll decide based on module system
2205+
source_file_abs = source_file.resolve()
21892206
tests_root_abs = tests_root.resolve()
21902207

21912208
# Find the project root using language support
@@ -2205,18 +2222,42 @@ def get_module_path(self, source_file: Path, project_root: Path, tests_root: Pat
22052222
if not tests_root_abs.exists():
22062223
tests_root_abs = project_root_from_lang
22072224

2225+
# Detect module system to determine if we need to add .js extension
2226+
module_system = detect_module_system(project_root, source_file)
2227+
2228+
# Remove source file extension first
2229+
source_without_ext = source_file_abs.with_suffix("")
2230+
22082231
# Use os.path.relpath to compute relative path from tests_root to source file
2209-
rel_path = os.path.relpath(str(source_file_abs), str(tests_root_abs))
2210-
logger.debug(
2211-
f"!lsp|Module path: source={source_file_abs}, tests_root={tests_root_abs}, rel_path={rel_path}"
2212-
)
2232+
rel_path = os.path.relpath(str(source_without_ext), str(tests_root_abs))
2233+
2234+
# For ESM, add .js extension (TypeScript convention)
2235+
# TypeScript requires imports to reference the OUTPUT file extension (.js),
2236+
# even when the source file is .ts. This is required for Node.js ESM resolution.
2237+
if module_system == ModuleSystem.ES_MODULE:
2238+
rel_path = rel_path + ".js"
2239+
logger.debug(
2240+
f"!lsp|Module path (ESM): source={source_file_abs}, tests_root={tests_root_abs}, "
2241+
f"rel_path={rel_path} (added .js for ESM)"
2242+
)
2243+
else:
2244+
logger.debug(
2245+
f"!lsp|Module path (CommonJS): source={source_file_abs}, tests_root={tests_root_abs}, "
2246+
f"rel_path={rel_path}"
2247+
)
2248+
22132249
return rel_path
22142250
except ValueError:
22152251
# Fallback if paths are on different drives (Windows)
22162252
rel_path = source_file.relative_to(project_root)
2217-
return "../" + rel_path.with_suffix("").as_posix()
2218-
2219-
def verify_requirements(self, project_root: Path, test_framework: str = "jest") -> tuple[bool, list[str]]:
2253+
# For fallback, also check module system
2254+
module_system = detect_module_system(project_root, source_file)
2255+
path_without_ext = "../" + rel_path.with_suffix("").as_posix()
2256+
if module_system == ModuleSystem.ES_MODULE:
2257+
return path_without_ext + ".js"
2258+
return path_without_ext
2259+
2260+
def verify_requirements(self, project_root: Path, test_framework: str = "jest") -> tuple[bool, list[SetupError]]:
22202261
"""Verify that all JavaScript requirements are met.
22212262
22222263
Checks for:
@@ -2236,27 +2277,40 @@ def verify_requirements(self, project_root: Path, test_framework: str = "jest")
22362277
Tuple of (success, list of error messages).
22372278
22382279
"""
2239-
errors: list[str] = []
2280+
errors: list[SetupError] = []
22402281

22412282
# Check Node.js
22422283
try:
22432284
result = subprocess.run(["node", "--version"], check=False, capture_output=True, text=True, timeout=10)
22442285
if result.returncode != 0:
2245-
errors.append("Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/")
2286+
errors.append(
2287+
SetupError(
2288+
"Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/",
2289+
should_abort=True,
2290+
)
2291+
)
22462292
except FileNotFoundError:
2247-
errors.append("Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/")
2293+
errors.append(
2294+
SetupError(
2295+
"Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/", should_abort=True
2296+
)
2297+
)
22482298
except Exception as e:
2249-
errors.append(f"Failed to check Node.js: {e}")
2299+
errors.append(SetupError(f"Failed to check Node.js: {e}", should_abort=True))
22502300

22512301
# Check npm
22522302
try:
22532303
result = subprocess.run(["npm", "--version"], check=False, capture_output=True, text=True, timeout=10)
22542304
if result.returncode != 0:
2255-
errors.append("npm is not available. Please ensure npm is installed with Node.js.")
2305+
errors.append(
2306+
SetupError("npm is not available. Please ensure npm is installed with Node.js.", should_abort=True)
2307+
)
22562308
except FileNotFoundError:
2257-
errors.append("npm is not available. Please ensure npm is installed with Node.js.")
2309+
errors.append(
2310+
SetupError("npm is not available. Please ensure npm is installed with Node.js.", should_abort=True)
2311+
)
22582312
except Exception as e:
2259-
errors.append(f"Failed to check npm: {e}")
2313+
errors.append(SetupError(f"Failed to check npm: {e}", should_abort=True))
22602314

22612315
# Check test framework is installed (with monorepo support)
22622316
# Uses find_node_modules_with_package which searches up the directory tree
@@ -2270,12 +2324,17 @@ def verify_requirements(self, project_root: Path, test_framework: str = "jest")
22702324
local_node_modules = project_root / "node_modules"
22712325
if not local_node_modules.exists():
22722326
errors.append(
2273-
f"node_modules not found in {project_root}. Please run 'npm install' to install dependencies."
2327+
SetupError(
2328+
f"node_modules not found in {project_root}. Please run 'npm install' to install dependencies.",
2329+
should_abort=True,
2330+
)
22742331
)
22752332
else:
22762333
errors.append(
2277-
f"{test_framework} is not installed. "
2278-
f"Please run 'npm install --save-dev {test_framework}' to install it."
2334+
SetupError(
2335+
f"{test_framework} is not installed. Please run 'npm install --save-dev {test_framework}' to install it.",
2336+
should_abort=True,
2337+
)
22792338
)
22802339

22812340
return len(errors) == 0, errors

codeflash/languages/javascript/treesitter.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,18 @@ def _walk_tree_for_functions(
290290
if func_info.is_method and node.parent and node.parent.type == "object":
291291
should_include = False
292292

293+
# Skip property getters/setters (e.g., get: function foo() {})
294+
# These are defined inside Object.defineProperty or object literals
295+
# and cannot be called directly - they're accessed via property names.
296+
# Tests would fail trying to call obj.getterFuncName() instead of obj.propertyName
297+
if node.type == "function_expression" and node.parent and node.parent.type == "pair":
298+
# Check if this is a getter or setter by looking at the property name
299+
property_name_node = node.parent.child_by_field_name("key")
300+
if property_name_node:
301+
property_name = self.get_node_text(property_name_node, source_bytes)
302+
if property_name in ("get", "set"):
303+
should_include = False
304+
293305
if should_include:
294306
functions.append(func_info)
295307

codeflash/languages/python/support.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,8 +1044,9 @@ def prepare_module(
10441044

10451045
pytest_cmd: str = "pytest"
10461046

1047-
def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> None:
1047+
def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> bool:
10481048
self.pytest_cmd = test_cfg.pytest_cmd or "pytest"
1049+
return True
10491050

10501051
def pytest_cmd_tokens(self, is_posix: bool) -> list[str]:
10511052
import shlex

codeflash/optimization/optimizer.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,11 @@ def run(self) -> None:
527527
if funcs and funcs[0].language:
528528
set_current_language(funcs[0].language)
529529
self.test_cfg.set_language(funcs[0].language)
530-
current_language_support().setup_test_config(self.test_cfg, file_path, self.current_worktree)
530+
if not current_language_support().setup_test_config(
531+
self.test_cfg, file_path, self.current_worktree
532+
):
533+
logger.error("Project setup failed — aborting optimization. Check warnings above for details.")
534+
return
531535
break
532536

533537
if self.args.all:

0 commit comments

Comments
 (0)