Skip to content

fix: resolve missing project JARs in Gradle multi-module classpath#1981

Open
HeshamHM28 wants to merge 4 commits intomainfrom
fix/gradle-classpath-missing-jars
Open

fix: resolve missing project JARs in Gradle multi-module classpath#1981
HeshamHM28 wants to merge 4 commits intomainfrom
fix/gradle-classpath-missing-jars

Conversation

@HeshamHM28
Copy link
Copy Markdown
Contributor

Summary

  • Classpath fix: Gradle's testRuntimeClasspath resolves project dependencies to JAR files (build/libs/*.jar), but testClasses only compiles classes — it doesn't build JARs. This caused direct JVM test execution to fail in multi-module projects (e.g. OpenRewrite: 11 missing JARs). New _resolve_project_classpath() detects missing project JARs and replaces them with build/classes/*/main + build/resources/main directories. Also compiles testRuntimeOnly project deps that testClasses skips.
  • Module detection fix: _extract_modules_from_settings_gradle() couldn't parse Kotlin-style listOf() variable declarations or multi-line include() calls (e.g. OpenRewrite's settings.gradle.kts). This caused the test runner to fall back to root-level Gradle execution, triggering unrelated subproject builds.

Verified against OpenRewrite (32-module Gradle project)

  • Before: 11 missing JARs → IllegalStateException: Unable to create a Java parser instance → all tests fail
  • After: 0 missing entries, 4 testRuntimeOnly modules auto-compiled, tests pass via direct JVM

Test plan

  • 8 new unit tests for _resolve_project_classpath and _compile_dependency_modules
  • 3 new unit tests for _extract_modules_from_settings_gradle (multi-line, listOf, Groovy)
  • All 57 existing Java test runner tests pass
  • End-to-end verification: AnnotationMatcherTest (RewriteTest framework, cross-module deps) passes via direct JVM on OpenRewrite
  • prek passes

🤖 Generated with Claude Code

Gradle's testRuntimeClasspath resolves project dependencies to JAR files
(build/libs/*.jar), but testClasses only compiles classes without building
JARs. This caused all tests to fail in multi-module projects like OpenRewrite
where 11 critical dependency JARs were missing from the classpath.

Changes:
- Add _resolve_project_classpath() to detect missing project JARs and replace
  them with build/classes/*/main + build/resources/main directories
- Add _compile_dependency_modules() to compile testRuntimeOnly project deps
  (e.g. rewrite-java-21) that testClasses skips
- Fix _extract_modules_from_settings_gradle() to parse multi-line include(),
  Kotlin listOf() variable declarations, and Groovy-style includes
- Remove old partial multi-module classpath supplement (superseded)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Apr 3, 2026

Claude finished @HeshamHM28's task in 8m 32s —— View job


PR Review Summary

Prek Checks

All checks pass — ruff and formatting clean. mypy reports no issues on the two changed production files.

All 61 tests (57 existing + 4 new in test_java_test_paths.py + 8 new in test_gradle_resolve_project_classpath.py) pass locally.


Code Review

Size: LARGE (176 lines of production code changed)

Bug: _scan_filesystem_for_modules has no error handling

test_runner.py:221-242 — the new fallback function iterates over directories without any protection:

for child in directory.iterdir():          # can raise PermissionError / OSError
    ...
    for grandchild in child.iterdir():     # can raise on broken symlinks

_detect_modules wraps the pom/settings file reads in try/except, but the filesystem fallback at line 269 is outside that guard. If any directory in the project root is unreadable (CI sandbox, broken symlink, network mount), this raises uncaught and propagates to the caller.

Fix this →

Potential file descriptor leak

gradle_strategy.py:676:

if not classes_dir.exists() or not any(classes_dir.iterdir()):

Path.iterdir() returns a _ScandirIter backed by os.scandir(). When any() short-circuits on the first entry, the iterator is left open (GC will close it eventually, but under load this can exhaust file descriptors). Prefer: next(classes_dir.iterdir(), None) is None.

Fix this →

Silent classpath entry drop on compilation failure

gradle_strategy.py:714-715: When _compile_dependency_modules fails silently (non-zero exit), Phase 3 still runs and drops the module's entry from the classpath (because classes_dir never got created). The resulting classpath is smaller than before, which will produce ClassNotFoundException rather than a missing JAR error — harder to diagnose. Consider preserving the original (non-existent) entry on added = False so the failure mode is at least as clear as before this PR.

Broad regex in _extract_modules_from_settings_gradle

test_runner.py:213 — the new regex ['"]([:\w][\w.:-]*)['"] is intentionally broad. Plugin IDs ("java", "application"), dependency group coordinates ("com.example"), and version strings ("1.0.0") all match. The docstring explains this is harmless because _match_module_from_rel_path filters against real paths. This is correct for well-structured projects. Edge case worth noting: a project with a first-party java/build.gradle.kts directory would have the plugin ID "java" spuriously match a real path — unlikely but possible in monorepos.


Duplicate Detection

No duplicates detected. The new _resolve_project_classpath / _compile_dependency_modules methods in gradle_strategy.py are Gradle-specific and don't duplicate logic in maven_strategy.py. The _scan_filesystem_for_modules function in test_runner.py is a new shared fallback, not a duplicate of anything existing.


Test Coverage

_scan_filesystem_for_modules (test_runner.py:221-242) has no direct unit tests — it's only exercised indirectly through _detect_modules. Given this function now runs for every project that has neither a readable pom.xml nor a parseable settings file, a direct test would help.

_compile_dependency_modules non-zero exit path (gradle_strategy.py:745-750) is not covered.

The added = False warning path in _resolve_project_classpath (gradle_strategy.py:714-715) is not covered — no test creates a module directory that is present but has neither build/classes/*/main nor build/resources/main.


Optimization PRs

Both open codeflash-ai[bot] PRs (#2022, #2024) had all CI checks passing and were mergeable — merged and branches deleted.

HeshamHM28 and others added 2 commits April 3, 2026 13:41
…tions

- Replace fragile multi-regex settings.gradle parser with a single broad
  pattern that extracts all quoted module identifiers, handling any DSL style
- Add _scan_filesystem_for_modules() as fallback when settings parsing fails
- Fix all test assertions to use full string equality (== []) not substring (in)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Hesham Mohamed <undefined@users.noreply.github.com>
@HeshamHM28 HeshamHM28 requested a review from mashraf-222 April 6, 2026 08:11
@HeshamHM28 HeshamHM28 enabled auto-merge April 7, 2026 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants