Skip to content

Commit cdaa526

Browse files
fix: align multi-language discovery with zero-config Java and add monorepo subdirectory scanning
Adapt find_all_config_files() after rebasing on java-config-redesign (PR #1880): - Java detected via pom.xml/build.gradle instead of codeflash.toml - Add subdirectory scan for monorepo language subprojects (java/, js/ etc.) - Extract _check_dir_for_configs() to eliminate duplicated detection logic - Fix --all flag in multi-language mode (module_root wasn't available during resolution) - Add Java project_root directory override in apply_language_config() - Update all tests to use build-tool detection mocks and directory-based Java paths - Add 5 new monorepo discovery tests (subdir Java, subdir JS, all-three, skip-hidden, root-wins) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e41517d commit cdaa526

5 files changed

Lines changed: 215 additions & 65 deletions

File tree

codeflash/cli_cmds/cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ def apply_language_config(args: Namespace, lang_config: LanguageConfig) -> Names
328328
args.benchmarks_root = Path(args.benchmarks_root).resolve()
329329
args.test_project_root = project_root_from_module_root(args.tests_root, config_path)
330330

331+
if is_java and config_path.is_dir():
332+
# For Java projects, config_path IS the project root directory (from build-tool detection).
333+
args.project_root = config_path.resolve()
334+
args.test_project_root = config_path.resolve()
335+
331336
return args
332337

333338

codeflash/code_utils/config_parser.py

Lines changed: 71 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -157,51 +157,92 @@ def normalize_toml_config(config: dict[str, Any], config_file_path: Path) -> dic
157157
return config
158158

159159

160+
def _parse_java_config_for_dir(dir_path: Path) -> dict[str, Any] | None:
161+
from codeflash.languages.java.build_tools import parse_java_project_config
162+
163+
return parse_java_project_config(dir_path)
164+
165+
166+
_SUBDIR_SKIP = frozenset({
167+
".git", ".hg", ".svn", "node_modules", ".venv", "venv", "__pycache__",
168+
"target", "build", "dist", ".tox", ".mypy_cache", ".ruff_cache", ".pytest_cache",
169+
})
170+
171+
172+
def _check_dir_for_configs(
173+
dir_path: Path,
174+
configs: list[LanguageConfig],
175+
seen_languages: set[Language],
176+
) -> None:
177+
"""Check a single directory for language config files and append any found to *configs*."""
178+
if Language.PYTHON not in seen_languages:
179+
pyproject = dir_path / "pyproject.toml"
180+
if pyproject.exists():
181+
try:
182+
with pyproject.open("rb") as f:
183+
data = tomlkit.parse(f.read())
184+
tool = data.get("tool", {})
185+
if isinstance(tool, dict) and "codeflash" in tool:
186+
raw_config = dict(tool["codeflash"])
187+
normalized = normalize_toml_config(raw_config, pyproject)
188+
seen_languages.add(Language.PYTHON)
189+
configs.append(LanguageConfig(config=normalized, config_path=pyproject, language=Language.PYTHON))
190+
except Exception:
191+
pass
192+
193+
if Language.JAVASCRIPT not in seen_languages:
194+
package_json = dir_path / "package.json"
195+
if package_json.exists():
196+
try:
197+
result = parse_package_json_config(package_json)
198+
if result is not None:
199+
config, path = result
200+
seen_languages.add(Language.JAVASCRIPT)
201+
configs.append(LanguageConfig(config=config, config_path=path, language=Language.JAVASCRIPT))
202+
except Exception:
203+
pass
204+
205+
if Language.JAVA not in seen_languages:
206+
if (
207+
(dir_path / "pom.xml").exists()
208+
or (dir_path / "build.gradle").exists()
209+
or (dir_path / "build.gradle.kts").exists()
210+
):
211+
try:
212+
java_config = _parse_java_config_for_dir(dir_path)
213+
if java_config is not None:
214+
seen_languages.add(Language.JAVA)
215+
configs.append(LanguageConfig(config=java_config, config_path=dir_path, language=Language.JAVA))
216+
except Exception:
217+
pass
218+
219+
160220
def find_all_config_files(start_dir: Path | None = None) -> list[LanguageConfig]:
161221
if start_dir is None:
162222
start_dir = Path.cwd()
163223

164224
configs: list[LanguageConfig] = []
165225
seen_languages: set[Language] = set()
166226

167-
toml_configs = {"pyproject.toml": Language.PYTHON, "codeflash.toml": Language.JAVA}
168-
227+
# Walk upward from start_dir to filesystem root (closest config wins per language)
169228
dir_path = start_dir.resolve()
170229
while True:
171-
for config_name, language in toml_configs.items():
172-
if language in seen_languages:
173-
continue
174-
config_file = dir_path / config_name
175-
if config_file.exists():
176-
try:
177-
with config_file.open("rb") as f:
178-
data = tomlkit.parse(f.read())
179-
tool = data.get("tool", {})
180-
if isinstance(tool, dict) and "codeflash" in tool:
181-
raw_config = dict(tool["codeflash"])
182-
normalized = normalize_toml_config(raw_config, config_file)
183-
seen_languages.add(language)
184-
configs.append(LanguageConfig(config=normalized, config_path=config_file, language=language))
185-
except Exception:
186-
continue
187-
188-
if Language.JAVASCRIPT not in seen_languages:
189-
package_json = dir_path / "package.json"
190-
if package_json.exists():
191-
try:
192-
result = parse_package_json_config(package_json)
193-
if result is not None:
194-
config, path = result
195-
seen_languages.add(Language.JAVASCRIPT)
196-
configs.append(LanguageConfig(config=config, config_path=path, language=Language.JAVASCRIPT))
197-
except Exception:
198-
pass
230+
_check_dir_for_configs(dir_path, configs, seen_languages)
199231

200232
parent = dir_path.parent
201233
if parent == dir_path:
202234
break
203235
dir_path = parent
204236

237+
# Scan immediate subdirectories for monorepo language subprojects
238+
resolved_start = start_dir.resolve()
239+
try:
240+
subdirs = sorted(p for p in resolved_start.iterdir() if p.is_dir() and p.name not in _SUBDIR_SKIP)
241+
except OSError:
242+
subdirs = []
243+
for subdir in subdirs:
244+
_check_dir_for_configs(subdir, configs, seen_languages)
245+
205246
return configs
206247

207248

codeflash/main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ def main() -> None:
131131
except UnsupportedLanguageError:
132132
pass # Unknown extension, let all configs run
133133

134+
# Track whether --all was originally requested (before handle_optimize_all_arg_parsing
135+
# resolves it — in multi-language mode, module_root isn't available yet so the resolution
136+
# produces None; we re-resolve per language inside the loop)
137+
optimize_all_requested = hasattr(args, "all") and args.all is not None
138+
134139
# Multi-language path: run git/GitHub checks ONCE before the loop
135140
args = handle_optimize_all_arg_parsing(args)
136141

@@ -141,7 +146,7 @@ def main() -> None:
141146
pass_args = copy.deepcopy(args)
142147
pass_args = apply_language_config(pass_args, lang_config)
143148

144-
if hasattr(pass_args, "all") and pass_args.all is not None:
149+
if optimize_all_requested:
145150
pass_args.all = pass_args.module_root
146151

147152
if not env_utils.check_formatter_installed(pass_args.formatter_cmds):

tests/test_multi_config_discovery.py

Lines changed: 115 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
import json
44
from pathlib import Path
5+
from unittest.mock import patch
56

67
import tomlkit
78

8-
from codeflash.code_utils.config_parser import LanguageConfig, find_all_config_files
9+
from codeflash.code_utils.config_parser import find_all_config_files
910
from codeflash.languages.language_enum import Language
1011

1112

@@ -22,19 +23,29 @@ def test_finds_pyproject_toml_with_codeflash_section(self, tmp_path: Path, monke
2223
assert result[0].language == Language.PYTHON
2324
assert result[0].config_path == tmp_path / "pyproject.toml"
2425

25-
def test_finds_codeflash_toml(self, tmp_path: Path, monkeypatch) -> None:
26-
write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}})
26+
def test_finds_java_via_build_tool_detection(self, tmp_path: Path, monkeypatch) -> None:
27+
java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")}
28+
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
2729
monkeypatch.chdir(tmp_path)
28-
result = find_all_config_files()
30+
with patch(
31+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
32+
return_value=java_config,
33+
):
34+
result = find_all_config_files()
2935
assert len(result) == 1
3036
assert result[0].language == Language.JAVA
31-
assert result[0].config_path == tmp_path / "codeflash.toml"
37+
assert result[0].config_path == tmp_path
3238

33-
def test_finds_multiple_configs(self, tmp_path: Path, monkeypatch) -> None:
39+
def test_finds_multiple_configs_python_and_java(self, tmp_path: Path, monkeypatch) -> None:
3440
write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}})
35-
write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}})
41+
java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")}
42+
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
3643
monkeypatch.chdir(tmp_path)
37-
result = find_all_config_files()
44+
with patch(
45+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
46+
return_value=java_config,
47+
):
48+
result = find_all_config_files()
3849
assert len(result) == 2
3950
languages = {r.language for r in result}
4051
assert languages == {Language.PYTHON, Language.JAVA}
@@ -49,9 +60,14 @@ def test_finds_config_in_parent_directory(self, tmp_path: Path, monkeypatch) ->
4960
write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}})
5061
subdir = tmp_path / "subproject"
5162
subdir.mkdir()
52-
write_toml(subdir / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}})
63+
java_config = {"language": "java", "module_root": str(subdir / "src/main/java")}
64+
(subdir / "pom.xml").write_text("<project/>", encoding="utf-8")
5365
monkeypatch.chdir(subdir)
54-
result = find_all_config_files()
66+
with patch(
67+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
68+
return_value=java_config,
69+
):
70+
result = find_all_config_files()
5571
assert len(result) == 2
5672
languages = {r.language for r in result}
5773
assert languages == {Language.PYTHON, Language.JAVA}
@@ -78,27 +94,111 @@ def test_finds_package_json_with_codeflash_section(self, tmp_path: Path, monkeyp
7894

7995
def test_finds_all_three_config_types(self, tmp_path: Path, monkeypatch) -> None:
8096
write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}})
81-
write_toml(tmp_path / "codeflash.toml", {"tool": {"codeflash": {"module-root": "src/main/java"}}})
8297
pkg = {"codeflash": {"moduleRoot": "src"}}
8398
(tmp_path / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
99+
java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")}
100+
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
84101
monkeypatch.chdir(tmp_path)
85-
result = find_all_config_files()
102+
with patch(
103+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
104+
return_value=java_config,
105+
):
106+
result = find_all_config_files()
86107
assert len(result) == 3
87108
languages = {r.language for r in result}
88109
assert languages == {Language.PYTHON, Language.JAVA, Language.JAVASCRIPT}
89110

90-
def test_malformed_toml_skipped(self, tmp_path: Path, monkeypatch) -> None:
91-
(tmp_path / "codeflash.toml").write_text("not valid [toml", encoding="utf-8")
111+
def test_no_java_when_no_build_file_exists(self, tmp_path: Path, monkeypatch) -> None:
92112
monkeypatch.chdir(tmp_path)
93113
result = find_all_config_files()
94114
assert len(result) == 0
95115

96116
def test_missing_codeflash_section_skipped(self, tmp_path: Path, monkeypatch) -> None:
97-
write_toml(tmp_path / "codeflash.toml", {"tool": {"other": {"key": "value"}}})
117+
write_toml(tmp_path / "pyproject.toml", {"tool": {"other": {"key": "value"}}})
118+
monkeypatch.chdir(tmp_path)
119+
result = find_all_config_files()
120+
assert len(result) == 0
121+
122+
def test_finds_java_in_subdirectory(self, tmp_path: Path, monkeypatch) -> None:
123+
"""Monorepo: Java project in a subdirectory is discovered from the repo root."""
124+
write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}})
125+
java_dir = tmp_path / "java"
126+
java_dir.mkdir()
127+
(java_dir / "pom.xml").write_text("<project/>", encoding="utf-8")
128+
java_config = {"language": "java", "module_root": str(java_dir / "src/main/java")}
129+
monkeypatch.chdir(tmp_path)
130+
with patch(
131+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
132+
return_value=java_config,
133+
):
134+
result = find_all_config_files()
135+
assert len(result) == 2
136+
languages = {r.language for r in result}
137+
assert languages == {Language.PYTHON, Language.JAVA}
138+
java_result = next(r for r in result if r.language == Language.JAVA)
139+
assert java_result.config_path == java_dir
140+
141+
def test_finds_js_in_subdirectory(self, tmp_path: Path, monkeypatch) -> None:
142+
"""Monorepo: JS project in a subdirectory is discovered from the repo root."""
143+
write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}})
144+
js_dir = tmp_path / "js"
145+
js_dir.mkdir()
146+
pkg = {"codeflash": {"moduleRoot": "src"}}
147+
(js_dir / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
148+
monkeypatch.chdir(tmp_path)
149+
result = find_all_config_files()
150+
assert len(result) == 2
151+
languages = {r.language for r in result}
152+
assert languages == {Language.PYTHON, Language.JAVASCRIPT}
153+
154+
def test_finds_all_three_in_monorepo_subdirs(self, tmp_path: Path, monkeypatch) -> None:
155+
"""Monorepo: Python at root, Java and JS in subdirectories."""
156+
write_toml(tmp_path / "pyproject.toml", {"tool": {"codeflash": {"module-root": "src"}}})
157+
java_dir = tmp_path / "java"
158+
java_dir.mkdir()
159+
(java_dir / "pom.xml").write_text("<project/>", encoding="utf-8")
160+
java_config = {"language": "java", "module_root": str(java_dir / "src/main/java")}
161+
js_dir = tmp_path / "js"
162+
js_dir.mkdir()
163+
pkg = {"codeflash": {"moduleRoot": "src"}}
164+
(js_dir / "package.json").write_text(json.dumps(pkg), encoding="utf-8")
165+
monkeypatch.chdir(tmp_path)
166+
with patch(
167+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
168+
return_value=java_config,
169+
):
170+
result = find_all_config_files()
171+
assert len(result) == 3
172+
languages = {r.language for r in result}
173+
assert languages == {Language.PYTHON, Language.JAVA, Language.JAVASCRIPT}
174+
175+
def test_skips_hidden_and_build_subdirs(self, tmp_path: Path, monkeypatch) -> None:
176+
"""Subdirectory scan skips .git, node_modules, target, etc."""
177+
for name in [".git", "node_modules", "target", "build", "__pycache__"]:
178+
d = tmp_path / name
179+
d.mkdir()
180+
write_toml(d / "pyproject.toml", {"tool": {"codeflash": {"module-root": "."}}})
98181
monkeypatch.chdir(tmp_path)
99182
result = find_all_config_files()
100183
assert len(result) == 0
101184

185+
def test_root_config_wins_over_subdir(self, tmp_path: Path, monkeypatch) -> None:
186+
"""Config at CWD (found during upward walk) takes precedence over subdirectory."""
187+
(tmp_path / "pom.xml").write_text("<project/>", encoding="utf-8")
188+
java_dir = tmp_path / "java"
189+
java_dir.mkdir()
190+
(java_dir / "pom.xml").write_text("<project/>", encoding="utf-8")
191+
java_config = {"language": "java", "module_root": str(tmp_path / "src/main/java")}
192+
monkeypatch.chdir(tmp_path)
193+
with patch(
194+
"codeflash.code_utils.config_parser._parse_java_config_for_dir",
195+
return_value=java_config,
196+
):
197+
result = find_all_config_files()
198+
java_results = [r for r in result if r.language == Language.JAVA]
199+
assert len(java_results) == 1
200+
assert java_results[0].config_path == tmp_path
201+
102202

103203
def test_find_all_functions_uses_registry_not_singleton() -> None:
104204
"""DISC-04: Verify find_all_functions_in_file uses per-file registry lookup."""

0 commit comments

Comments
 (0)