Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions codeflash/languages/java/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ def _should_include_method(
True if the method should be included.

"""
# Skip methods that belong to an inner/nested class — they cannot be reliably
# instrumented or tested in isolation (see discussion in discovery module).
if method.is_class_nested:
return False

# Skip abstract methods (no implementation to optimize)
if method.is_abstract:
return False
Expand Down
108 changes: 107 additions & 1 deletion codeflash/languages/java/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class JavaMethodNode:
class_name: str | None
source_text: str
javadoc_start_line: int | None = None # Line where Javadoc comment starts
formal_parameters_text: str | None = None # Raw formal parameters "(Type name, ...)" for matching
is_class_nested: bool = False # True when the enclosing class is itself nested inside another class


@dataclass
Expand Down Expand Up @@ -182,6 +184,104 @@ def find_methods(

return methods

def find_constructors(self, source: str, class_name: str | None = None) -> list[JavaMethodNode]:
"""Find all constructor definitions in source code.

Args:
source: The source code to analyze.
class_name: Optional class name to filter constructors.

Returns:
List of JavaMethodNode objects describing found constructors.
The ``name`` field of each node is the constructor name (i.e. the class name).

"""
source_bytes = source.encode("utf8")
tree = self.parse(source_bytes)
constructors: list[JavaMethodNode] = []
self._walk_tree_for_constructors(
tree.root_node, source_bytes, constructors, current_class=None, target_class=class_name
)
return constructors

def _walk_tree_for_constructors(
self,
node: Node,
source_bytes: bytes,
constructors: list[JavaMethodNode],
current_class: str | None,
target_class: str | None,
) -> None:
"""Recursively walk the tree to find constructor declarations."""
new_class = current_class
type_declarations = ("class_declaration", "interface_declaration", "enum_declaration")
if node.type in type_declarations:
name_node = node.child_by_field_name("name")
if name_node:
new_class = self.get_node_text(name_node, source_bytes)

if node.type == "constructor_declaration":
constructor_info = self._extract_constructor_info(node, source_bytes, new_class)
if constructor_info:
if target_class is None or constructor_info.class_name == target_class:
constructors.append(constructor_info)

for child in node.children:
self._walk_tree_for_constructors(
child,
source_bytes,
constructors,
current_class=new_class if node.type in type_declarations else current_class,
target_class=target_class,
)

def _extract_constructor_info(
self, node: Node, source_bytes: bytes, current_class: str | None
) -> JavaMethodNode | None:
"""Extract constructor information from a constructor_declaration node."""
name_node = node.child_by_field_name("name")
if not name_node:
return None
name = self.get_node_text(name_node, source_bytes)

is_public = False
is_private = False
is_protected = False
for child in node.children:
if child.type == "modifiers":
modifier_text = self.get_node_text(child, source_bytes)
is_public = "public" in modifier_text
is_private = "private" in modifier_text
is_protected = "protected" in modifier_text
break

# Extract formal parameters text for signature matching
params_node = node.child_by_field_name("parameters")
formal_parameters_text = self.get_node_text(params_node, source_bytes) if params_node else "()"

source_text = self.get_node_text(node, source_bytes)
javadoc_start_line = self._find_preceding_javadoc(node, source_bytes)

return JavaMethodNode(
name=name,
node=node,
start_line=node.start_point[0] + 1,
end_line=node.end_point[0] + 1,
start_col=node.start_point[1],
end_col=node.end_point[1],
is_static=False,
is_public=is_public,
is_private=is_private,
is_protected=is_protected,
is_abstract=False,
is_synchronized=False,
return_type=None,
class_name=current_class,
source_text=source_text,
javadoc_start_line=javadoc_start_line,
formal_parameters_text=formal_parameters_text,
)

def _walk_tree_for_methods(
self,
node: Node,
Expand All @@ -190,6 +290,7 @@ def _walk_tree_for_methods(
include_private: bool,
include_static: bool,
current_class: str | None,
class_depth: int = 0,
) -> None:
"""Recursively walk the tree to find method definitions."""
new_class = current_class
Expand All @@ -205,6 +306,10 @@ def _walk_tree_for_methods(
method_info = self._extract_method_info(node, source_bytes, current_class)

if method_info:
# A method is nested when its enclosing class is itself inside another
# class (class_depth >= 2: depth 1 = outermost class, depth 2+ = nested).
method_info.is_class_nested = class_depth >= 2

# Apply filters
should_include = True

Expand All @@ -217,7 +322,7 @@ def _walk_tree_for_methods(
if should_include:
methods.append(method_info)

# Recurse into children
# Recurse into children, incrementing depth when entering a type declaration
for child in node.children:
self._walk_tree_for_methods(
child,
Expand All @@ -226,6 +331,7 @@ def _walk_tree_for_methods(
include_private=include_private,
include_static=include_static,
current_class=new_class if node.type in type_declarations else current_class,
class_depth=class_depth + 1 if node.type in type_declarations else class_depth,
)

def _extract_method_info(self, node: Node, source_bytes: bytes, current_class: str | None) -> JavaMethodNode | None:
Expand Down
119 changes: 116 additions & 3 deletions codeflash/languages/java/replacement.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@ class ParsedOptimization:
new_fields: list[str] # Source text of new fields to add
helpers_before_target: list[str] = field(default_factory=list) # Helpers appearing before target in optimized code
helpers_after_target: list[str] = field(default_factory=list) # Helpers appearing after target in optimized code
modified_constructors: list[str] = field(default_factory=list) # Constructor sources that need to replace originals


def _parse_optimization_source(new_source: str, target_method_name: str, analyzer: JavaAnalyzer) -> ParsedOptimization:
def _parse_optimization_source(
new_source: str,
target_method_name: str,
analyzer: JavaAnalyzer,
) -> ParsedOptimization:
"""Parse optimization source to extract method and additional class members.

The new_source may contain:
Expand All @@ -63,6 +68,7 @@ def _parse_optimization_source(new_source: str, target_method_name: str, analyze

helpers_before_target: list[str] = []
helpers_after_target: list[str] = []
modified_constructors: list[str] = []

if classes:
# It's a class - extract components
Expand Down Expand Up @@ -112,6 +118,22 @@ def _parse_optimization_source(new_source: str, target_method_name: str, analyze
else:
helpers_after_target.append(helper_source)

# Extract constructors that belong to the same class as the target method.
# When the LLM adds a new field (e.g. a cached value), it also updates the
# constructors to initialize it. We must replace those constructors in the
# original source, otherwise the new final field will be uninitialized
# (Bug 3: uninitialized variable errors).
# Use line-sliced text (same as helper methods) so that the leading whitespace
# is preserved and _dedent_member can normalise indentation correctly.
if target_method:
target_class_name_for_ctors = target_method.class_name
new_constructors = analyzer.find_constructors(new_source, class_name=target_class_name_for_ctors)
ctor_lines = new_source.splitlines(keepends=True)
for c in new_constructors:
ctor_start = (c.javadoc_start_line or c.start_line) - 1
ctor_end = c.end_line
modified_constructors.append("".join(ctor_lines[ctor_start:ctor_end]))

# Extract fields
for f in fields:
if f.source_text:
Expand All @@ -138,6 +160,7 @@ def _parse_optimization_source(new_source: str, target_method_name: str, analyze
new_fields=new_fields,
helpers_before_target=helpers_before_target,
helpers_after_target=helpers_after_target,
modified_constructors=modified_constructors,
)


Expand Down Expand Up @@ -272,6 +295,89 @@ def format_member(raw: str) -> str:
return result


def _replace_constructors(
source: str,
class_name: str,
new_constructor_sources: list[str],
analyzer: JavaAnalyzer,
) -> str:
"""Replace constructors in source with updated versions from the optimization.

Matches constructors by their formal parameter signature. When a matching
constructor is found in the original source it is replaced in-place,
preserving the original indentation. Constructors for which no match
exists in the original are silently skipped (they would need to be inserted
as new members, which is out of scope for this helper).

Args:
source: The original source code to modify.
class_name: Name of the class whose constructors should be replaced.
new_constructor_sources: Source text of each updated constructor.
analyzer: JavaAnalyzer instance.

Returns:
Modified source code with constructors replaced.

"""
if not new_constructor_sources:
return source

original_constructors = analyzer.find_constructors(source, class_name=class_name)
if not original_constructors:
return source

result = source

for new_ctor_src in new_constructor_sources:
# Wrap in a dummy class so the parser can handle a bare constructor
dummy = f"class __Dummy__ {{\n{new_ctor_src}\n}}"
parsed_new = analyzer.find_constructors(dummy)
if not parsed_new:
continue
new_ctor = parsed_new[0]
new_params = (new_ctor.formal_parameters_text or "()").strip()

# Find the matching constructor in the current (potentially already
# modified) source by parameter signature.
current_constructors = analyzer.find_constructors(result, class_name=class_name)
matching = None
for orig in current_constructors:
if (orig.formal_parameters_text or "()").strip() == new_params:
matching = orig
break

if not matching:
logger.debug(
"No matching constructor with params %s found in class %s; skipping.",
new_params,
class_name,
)
continue

# Determine replacement range (include Javadoc if present)
ctor_start = matching.javadoc_start_line or matching.start_line
ctor_end = matching.end_line

lines = result.splitlines(keepends=True)
original_first_line = lines[ctor_start - 1] if ctor_start <= len(lines) else ""
indent = _get_indentation(original_first_line)

# Dedent first to remove any class-level indentation, then re-apply
# the correct indentation (same as _insert_class_members / format_member).
new_ctor_lines = _dedent_member(new_ctor_src).splitlines(keepends=True)
indented_new_ctor = _apply_indentation(new_ctor_lines, indent)
if indented_new_ctor and not indented_new_ctor.endswith("\n"):
indented_new_ctor += "\n"

before = lines[: ctor_start - 1]
after = lines[ctor_end:]
result = "".join(before) + indented_new_ctor + "".join(after)

logger.debug("Replaced constructor %s(%s) in class %s", class_name, new_params, class_name)

return result


def replace_function(
source: str, function: FunctionToOptimize, new_source: str, analyzer: JavaAnalyzer | None = None
) -> str:
Expand Down Expand Up @@ -305,7 +411,7 @@ def replace_function(
func_start_line = function.starting_line
func_end_line = function.ending_line

# Parse the optimization to extract components
# Parse the optimization to extract components.
parsed = _parse_optimization_source(new_source, func_name, analyzer)

# If the parsed optimization has no valid target source (e.g., the LLM generated
Expand Down Expand Up @@ -467,7 +573,14 @@ def replace_function(
before = lines[: start_line - 1] # Lines before the method
after = lines[end_line:] # Lines after the method

return "".join(before) + indented_new_source + "".join(after)
result = "".join(before) + indented_new_source + "".join(after)

# Replace modified constructors if the optimization introduced new field
# initializations (Bug 3: uninitialized variable errors).
if class_name and parsed.modified_constructors:
result = _replace_constructors(result, class_name, parsed.modified_constructors, analyzer)

return result


def _get_indentation(line: str) -> str:
Expand Down
Loading
Loading