Skip to content

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

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

fix: resolve missing project JARs in Gradle multi-module classpath#1981
HeshamHM28 wants to merge 3 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 4m 40s —— View job


PR Review Summary

Tasks

  • Triage PR scope → LARGE (131 lines of production code)
  • Lint and typecheck → 1 issue fixed (_BUILD_FILESbuild_files, ruff N806)
  • Resolve stale threads → none found
  • Code review
  • Duplicate detection
  • Test coverage

Prek Checks

Fixed automatically: N806 — uppercase _BUILD_FILES used as a local variable in _scan_filesystem_for_modules (should be lowercase per PEP 8 / ruff). Renamed to build_files and pushed in commit 3a54a740.

Mypy: clean, no issues.


Code Review

Bug: missing JAR silently dropped when no replacement is found

gradle_strategy.py:713-714 — when a JAR module is identified as missing but Phase 2 compilation fails (or the module produced no class/resource dirs), the original JAR entry is silently dropped from the classpath. The warning is logged but no entry is emitted:

if not added:
    logger.warning("No class/resource directories found for missing JAR: %s", entry)
    # entry is omitted entirely — nothing appended to result_entries

A missing JAR is better than a missing classpath entry: the JVM will fail with a clear ClassNotFoundException referencing a specific class. A missing entry will fail with the same error but the classpath is now shorter, making it harder to diagnose. Consider appending the original entry as fallback:

if not added:
    logger.warning("No class/resource directories found for missing JAR: %s — keeping original entry", entry)
    result_entries.append(entry)  # keep original even if JAR doesn't exist

Fix this →

Design: _resolve_project_classpath removed previous multi-module classpath logic

The diff removes this block from _get_classpath_uncached (lines that previously added build/classes/java/main from sibling modules):

# REMOVED:
if test_module:
    module_dir_name = module_to_dir(test_module)
    for module_dir in build_root.iterdir():
        if module_dir.is_dir() and module_dir.name != module_dir_name:
            module_classes = module_dir / "build" / "classes" / "java" / "main"
            if module_classes.exists():
                cp_parts.append(str(module_classes))

The new _resolve_project_classpath correctly handles this via the testRuntimeClasspath, but only replaces modules that appear as missing JARs in the Gradle-resolved classpath. The old code unconditionally added ALL sibling module class dirs. If a sibling module's JAR exists on disk (i.e., it was fully built), _resolve_project_classpath won't touch it — meaning sibling modules that ARE packaged won't be duplicated in the classpath. That's correct. Just flagging this as an intentional behavior change worth confirming.

Minor: startswith path prefix matching

gradle_strategy.py:662:

if not entry.startswith(build_root_str) or build_libs_sep not in entry:

build_root_str is str(build_root) without a trailing separator. A path like /home/user/project-extra/module/build/libs/foo.jar would match if build_root_str = /home/user/project. A false match is harmless because the subsequent module_dir.is_dir() check will reject it if the extracted path doesn't exist. But consider using entry.startswith(build_root_str + os.sep) or Path(entry).is_relative_to(build_root) (Python 3.9+) for correctness.

Style: docstrings on new methods

CLAUDE.md prohibits adding docstrings to new or changed code unless explicitly requested. All three new methods (_resolve_project_classpath, _compile_dependency_modules, _scan_filesystem_for_modules) have multi-line docstrings. These should be removed.


Duplicate Detection

No duplicates detected. The new functions are Java-specific and do not overlap with Python/JavaScript parallel modules or existing utilities.


Test Coverage

All 61 tests pass (57 existing + 4 new split across the two test files). The new test file test_gradle_resolve_project_classpath.py covers all major branches of _resolve_project_classpath and _compile_dependency_modules. The Kotlin listOf, multi-line, Groovy, and dedup cases in _extract_modules_from_settings_gradle are well covered. One coverage gap worth noting: _compile_dependency_modules failure path (Gradle exits non-zero) has no test, though the happy path is covered.


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>
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.

1 participant