Skip to content

Commit 7950915

Browse files
committed
wip e2e
1 parent 74c4035 commit 7950915

11 files changed

Lines changed: 192 additions & 40 deletions

File tree

codeflash/api/aiservice.py

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,28 @@ def _get_valid_candidates(
120120
)
121121
return candidates
122122

123-
def optimize_python_code( # noqa: D417
123+
def optimize_code( # noqa: D417
124124
self,
125125
source_code: str,
126126
dependency_code: str,
127127
trace_id: str,
128128
experiment_metadata: ExperimentMetadata | None = None,
129129
*,
130+
language: str = "python",
131+
language_version: str | None = None,
130132
is_async: bool = False,
131133
n_candidates: int = 5,
132134
) -> list[OptimizedCandidate]:
133-
"""Optimize the given python code for performance by making a request to the Django endpoint.
135+
"""Optimize the given code for performance by making a request to the Django endpoint.
134136
135137
Parameters
136138
----------
137-
- source_code (str): The python code to optimize.
139+
- source_code (str): The code to optimize.
138140
- dependency_code (str): The dependency code used as read-only context for the optimization
139141
- trace_id (str): Trace id of optimization run
140142
- experiment_metadata (Optional[ExperimentalMetadata, None]): Any available experiment metadata for this optimization
143+
- language (str): Programming language ("python", "javascript", "typescript")
144+
- language_version (str | None): Language version (e.g., "3.11.0" for Python, "ES2022" for JS)
141145
- is_async (bool): Whether the function being optimized is async
142146
- n_candidates (int): Number of candidates to generate
143147
@@ -151,11 +155,12 @@ def optimize_python_code( # noqa: D417
151155
start_time = time.perf_counter()
152156
git_repo_owner, git_repo_name = safe_get_repo_owner_and_name()
153157

154-
payload = {
158+
# Build payload with language-specific fields
159+
payload: dict[str, Any] = {
155160
"source_code": source_code,
156161
"dependency_code": dependency_code,
157162
"trace_id": trace_id,
158-
"python_version": platform.python_version(),
163+
"language": language,
159164
"experiment_metadata": experiment_metadata,
160165
"codeflash_version": codeflash_version,
161166
"current_username": get_last_commit_author_if_pr_exists(None),
@@ -165,6 +170,14 @@ def optimize_python_code( # noqa: D417
165170
"call_sequence": self.get_next_sequence(),
166171
"n_candidates": n_candidates,
167172
}
173+
174+
# Add language-specific version fields
175+
# Always include python_version for backward compatibility with older backend
176+
payload["python_version"] = platform.python_version()
177+
if language == "python":
178+
pass # python_version already set
179+
else:
180+
payload["language_version"] = language_version or "ES2022"
168181
logger.debug(f"Sending optimize request: trace_id={trace_id}, n_candidates={payload['n_candidates']}")
169182

170183
try:
@@ -191,6 +204,28 @@ def optimize_python_code( # noqa: D417
191204
console.rule()
192205
return []
193206

207+
# Backward-compatible alias
208+
def optimize_python_code(
209+
self,
210+
source_code: str,
211+
dependency_code: str,
212+
trace_id: str,
213+
experiment_metadata: ExperimentMetadata | None = None,
214+
*,
215+
is_async: bool = False,
216+
n_candidates: int = 5,
217+
) -> list[OptimizedCandidate]:
218+
"""Backward-compatible alias for optimize_code() with language='python'."""
219+
return self.optimize_code(
220+
source_code=source_code,
221+
dependency_code=dependency_code,
222+
trace_id=trace_id,
223+
experiment_metadata=experiment_metadata,
224+
language="python",
225+
is_async=is_async,
226+
n_candidates=n_candidates,
227+
)
228+
194229
def optimize_python_code_line_profiler( # noqa: D417
195230
self,
196231
source_code: str,
@@ -578,6 +613,9 @@ def generate_regression_tests( # noqa: D417
578613
test_timeout: int,
579614
trace_id: str,
580615
test_index: int,
616+
*,
617+
language: str = "python",
618+
language_version: str | None = None,
581619
) -> tuple[str, str, str] | None:
582620
"""Generate regression tests for the given function by making a request to the Django endpoint.
583621
@@ -588,19 +626,30 @@ def generate_regression_tests( # noqa: D417
588626
- helper_function_names (list[Source]): List of helper function names.
589627
- module_path (Path): The module path where the function is located.
590628
- test_module_path (Path): The module path for the test code.
591-
- test_framework (str): The test framework to use, e.g., "pytest".
629+
- test_framework (str): The test framework to use, e.g., "pytest", "jest".
592630
- test_timeout (int): The timeout for each test in seconds.
593631
- test_index (int): The index from 0-(n-1) if n tests are generated for a single trace_id
632+
- language (str): Programming language ("python", "javascript", "typescript")
633+
- language_version (str | None): Language version (e.g., "3.11.0" for Python, "ES2022" for JS)
594634
595635
Returns
596636
-------
597637
- Dict[str, str] | None: The generated regression tests and instrumented tests, or None if an error occurred.
598638
599639
"""
600-
assert test_framework in ["pytest", "unittest"], (
601-
f"Invalid test framework, got {test_framework} but expected 'pytest' or 'unittest'"
602-
)
603-
payload = {
640+
# Validate test framework based on language
641+
python_frameworks = ["pytest", "unittest"]
642+
javascript_frameworks = ["jest", "mocha", "vitest"]
643+
if language == "python":
644+
assert test_framework in python_frameworks, (
645+
f"Invalid test framework for Python, got {test_framework} but expected one of {python_frameworks}"
646+
)
647+
elif language in ("javascript", "typescript"):
648+
assert test_framework in javascript_frameworks, (
649+
f"Invalid test framework for JavaScript, got {test_framework} but expected one of {javascript_frameworks}"
650+
)
651+
652+
payload: dict[str, Any] = {
604653
"source_code_being_tested": source_code_being_tested,
605654
"function_to_optimize": function_to_optimize,
606655
"helper_function_names": helper_function_names,
@@ -610,11 +659,19 @@ def generate_regression_tests( # noqa: D417
610659
"test_timeout": test_timeout,
611660
"trace_id": trace_id,
612661
"test_index": test_index,
613-
"python_version": platform.python_version(),
662+
"language": language,
614663
"codeflash_version": codeflash_version,
615664
"is_async": function_to_optimize.is_async,
616665
"call_sequence": self.get_next_sequence(),
617666
}
667+
668+
# Add language-specific version fields
669+
# Always include python_version for backward compatibility with older backend
670+
payload["python_version"] = platform.python_version()
671+
if language == "python":
672+
pass # python_version already set
673+
else:
674+
payload["language_version"] = language_version or "ES2022"
618675
try:
619676
response = self.make_ai_service_request("/testgen", payload=payload, timeout=self.timeout)
620677
except requests.exceptions.RequestException as e:

codeflash/code_utils/code_extractor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1149,9 +1149,12 @@ def get_fn_references_jedi(
11491149
source_code: str, file_path: Path, project_root: Path, target_function: str, target_class: str | None
11501150
) -> list[Path]:
11511151
start_time = time.perf_counter()
1152-
function_position: CodePosition = find_specific_function_in_file(
1152+
function_position: CodePosition | None = find_specific_function_in_file(
11531153
source_code, file_path, target_function, target_class
11541154
)
1155+
if function_position is None:
1156+
# Function not found (may be non-Python code)
1157+
return []
11551158
try:
11561159
script = jedi.Script(code=source_code, path=file_path, project=jedi.Project(path=project_root))
11571160
# Get references to the function

codeflash/code_utils/code_replacer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,8 @@ def replace_optimized_code(
601601
[
602602
callee.qualified_name
603603
for callee in code_context.helper_functions
604-
if callee.file_path == module_path and callee.jedi_definition.type != "class"
604+
if callee.file_path == module_path
605+
and (callee.jedi_definition is None or callee.jedi_definition.type != "class")
605606
]
606607
),
607608
candidate.source_code,

codeflash/context/unused_definition_remover.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -632,8 +632,8 @@ def _analyze_imports_in_optimized_code(
632632
helpers_by_file_and_func = defaultdict(dict)
633633
helpers_by_file = defaultdict(list) # preserved for "import module"
634634
for helper in code_context.helper_functions:
635-
jedi_type = helper.jedi_definition.type
636-
if jedi_type != "class":
635+
jedi_type = helper.jedi_definition.type if helper.jedi_definition else None
636+
if jedi_type != "class": # Include when jedi_definition is None (non-Python)
637637
func_name = helper.only_function_name
638638
module_name = helper.file_path.stem
639639
# Cache function lookup for this (module, func)
@@ -784,7 +784,8 @@ def detect_unused_helper_functions(
784784
# Find helper functions that are no longer called
785785
unused_helpers = []
786786
for helper_function in code_context.helper_functions:
787-
if helper_function.jedi_definition.type != "class":
787+
jedi_type = helper_function.jedi_definition.type if helper_function.jedi_definition else None
788+
if jedi_type != "class": # Include when jedi_definition is None (non-Python)
788789
# Check if the helper function is called using multiple name variants
789790
helper_qualified_name = helper_function.qualified_name
790791
helper_simple_name = helper_function.only_function_name

codeflash/discovery/discover_unit_tests.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,6 @@ def discover_tests_for_language(
616616
end_col=func.ending_col,
617617
is_async=func.is_async,
618618
is_method=bool(func.parents and any(p.type == "ClassDef" for p in func.parents)),
619-
class_name=func.parents[0].name if func.parents and func.parents[0].type == "ClassDef" else None,
620619
parents=parents,
621620
language=Language(language),
622621
)

codeflash/models/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ class FunctionSource:
129129
fully_qualified_name: str
130130
only_function_name: str
131131
source_code: str
132-
jedi_definition: Name
132+
jedi_definition: Name | None = None # None for non-Python languages
133133

134134
def __eq__(self, other: object) -> bool:
135135
if not isinstance(other, FunctionSource):
@@ -323,10 +323,10 @@ def parse_markdown_code(markdown_code: str, expected_language: str = "python") -
323323
detected_language = language
324324
if file_path:
325325
path = file_path.strip()
326-
code_string_list.append(CodeString(code=code, file_path=Path(path)))
326+
code_string_list.append(CodeString(code=code, file_path=Path(path), language=detected_language))
327327
else:
328328
# No file path specified - skip this block or create with None
329-
code_string_list.append(CodeString(code=code, file_path=None))
329+
code_string_list.append(CodeString(code=code, file_path=None, language=detected_language))
330330
return CodeStringsMarkdown(code_strings=code_string_list, language=detected_language)
331331
except ValidationError:
332332
# if any file is invalid, return an empty CodeStringsMarkdown for the entire context

codeflash/optimization/function_optimizer.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,14 @@ def __init__(
434434
else function_to_optimize.file_path.read_text(encoding="utf8")
435435
)
436436
if not function_to_optimize_ast:
437-
original_module_ast = ast.parse(function_to_optimize_source_code)
438-
self.function_to_optimize_ast = get_first_top_level_function_or_method_ast(
439-
function_to_optimize.function_name, function_to_optimize.parents, original_module_ast
440-
)
437+
# Skip Python AST parsing for JavaScript/TypeScript
438+
if function_to_optimize.language in ("javascript", "typescript"):
439+
self.function_to_optimize_ast = None
440+
else:
441+
original_module_ast = ast.parse(function_to_optimize_source_code)
442+
self.function_to_optimize_ast = get_first_top_level_function_or_method_ast(
443+
function_to_optimize.function_name, function_to_optimize.parents, original_module_ast
444+
)
441445
else:
442446
self.function_to_optimize_ast = function_to_optimize_ast
443447
self.function_to_tests = function_to_tests if function_to_tests else {}
@@ -450,7 +454,26 @@ def __init__(
450454

451455
self.args = args # Check defaults for these
452456
self.function_trace_id: str = str(uuid.uuid4())
453-
self.original_module_path = module_name_from_file_path(self.function_to_optimize.file_path, self.project_root)
457+
# For JavaScript/TypeScript, we need a relative path from the test file to the source file
458+
# For Python, we use dot-separated module paths
459+
if self.function_to_optimize.language in ("javascript", "typescript"):
460+
# Compute relative path from tests directory to source file
461+
# e.g., for source at /project/fibonacci.js and tests at /project/tests/
462+
# the relative path should be ../fibonacci
463+
try:
464+
# Use os.path.relpath to compute relative path from tests_root to source file
465+
import os
466+
rel_path = os.path.relpath(
467+
str(self.function_to_optimize.file_path.with_suffix("")),
468+
str(test_cfg.tests_root)
469+
)
470+
self.original_module_path = rel_path
471+
except ValueError:
472+
# Fallback if paths are on different drives (Windows)
473+
rel_path = self.function_to_optimize.file_path.relative_to(self.project_root)
474+
self.original_module_path = "../" + rel_path.with_suffix("").as_posix()
475+
else:
476+
self.original_module_path = module_name_from_file_path(self.function_to_optimize.file_path, self.project_root)
454477

455478
self.function_benchmark_timings = function_benchmark_timings if function_benchmark_timings else {}
456479
self.total_benchmark_timings = total_benchmark_timings if total_benchmark_timings else {}
@@ -510,15 +533,24 @@ def generate_and_instrument_tests(
510533
]:
511534
"""Generate and instrument tests for the function."""
512535
n_tests = get_effort_value(EffortKeys.N_GENERATED_TESTS, self.effort)
536+
language = self.function_to_optimize.language
513537
generated_test_paths = [
514538
get_test_file_path(
515-
self.test_cfg.tests_root, self.function_to_optimize.function_name, test_index, test_type="unit"
539+
self.test_cfg.tests_root,
540+
self.function_to_optimize.function_name,
541+
test_index,
542+
test_type="unit",
543+
language=language,
516544
)
517545
for test_index in range(n_tests)
518546
]
519547
generated_perf_test_paths = [
520548
get_test_file_path(
521-
self.test_cfg.tests_root, self.function_to_optimize.function_name, test_index, test_type="perf"
549+
self.test_cfg.tests_root,
550+
self.function_to_optimize.function_name,
551+
test_index,
552+
test_type="perf",
553+
language=language,
522554
)
523555
for test_index in range(n_tests)
524556
]
@@ -1370,7 +1402,8 @@ def replace_function_and_helpers_with_optimized_code(
13701402
self.function_to_optimize.qualified_name
13711403
)
13721404
for helper_function in code_context.helper_functions:
1373-
if helper_function.jedi_definition.type != "class":
1405+
# Skip class definitions (jedi_definition may be None for non-Python languages)
1406+
if helper_function.jedi_definition is None or helper_function.jedi_definition.type != "class":
13741407
read_writable_functions_by_file_path[helper_function.file_path].add(helper_function.qualified_name)
13751408
for module_abspath, qualified_names in read_writable_functions_by_file_path.items():
13761409
did_update |= replace_function_definitions_in_module(
@@ -1423,6 +1456,12 @@ def instrument_existing_tests(self, function_to_all_tests: dict[str, set[Functio
14231456
func_qualname = self.function_to_optimize.qualified_name_with_modules_from_root(self.project_root)
14241457
if func_qualname not in function_to_all_tests:
14251458
logger.info(f"Did not find any pre-existing tests for '{func_qualname}', will only use generated tests.")
1459+
# Skip existing test instrumentation for JavaScript/TypeScript - use generated tests only
1460+
elif self.function_to_optimize.language in ("javascript", "typescript"):
1461+
logger.info(
1462+
f"JavaScript/TypeScript detected - using generated tests only for '{func_qualname}'. "
1463+
"Existing test instrumentation not yet supported."
1464+
)
14261465
else:
14271466
test_file_invocation_positions = defaultdict(list)
14281467
for tests_in_file in function_to_all_tests.get(func_qualname):
@@ -1577,11 +1616,12 @@ def generate_optimizations(
15771616
"""Generate optimization candidates for the function. Backend handles multi-model diversity."""
15781617
n_candidates = get_effort_value(EffortKeys.N_OPTIMIZER_CANDIDATES, self.effort)
15791618
future_optimization_candidates = self.executor.submit(
1580-
self.aiservice_client.optimize_python_code,
1619+
self.aiservice_client.optimize_code,
15811620
read_writable_code.markdown,
15821621
read_only_context_code,
15831622
self.function_trace_id[:-4] + "EXP0" if run_experiment else self.function_trace_id,
15841623
ExperimentMetadata(id=self.experiment_id, group="control") if run_experiment else None,
1624+
language=self.function_to_optimize.language,
15851625
is_async=self.function_to_optimize.is_async,
15861626
n_candidates=n_candidates,
15871627
)
@@ -1600,11 +1640,12 @@ def generate_optimizations(
16001640

16011641
if run_experiment:
16021642
future_candidates_exp = self.executor.submit(
1603-
self.local_aiservice_client.optimize_python_code,
1643+
self.local_aiservice_client.optimize_code,
16041644
read_writable_code.markdown,
16051645
read_only_context_code,
16061646
self.function_trace_id[:-4] + "EXP1",
16071647
ExperimentMetadata(id=self.experiment_id, group="experiment"),
1648+
language=self.function_to_optimize.language,
16081649
is_async=self.function_to_optimize.is_async,
16091650
n_candidates=n_candidates,
16101651
)

0 commit comments

Comments
 (0)