From c3f15f703b3477767543bef646dd8cdbace06f32 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 17 May 2026 01:12:34 +0800 Subject: [PATCH] feat(detect): add JVM Gradle defaults --- CHANGELOG.md | 1 + README.md | 1 + docs/feature-mapping.md | 3 ++- docs/quickstart.md | 1 + src/detect.ts | 50 +++++++++++++++++++++++++++++++++++++++- src/mapper.test.ts | 51 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af9164f..54be7a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Detected Java/Kotlin language and default Gradle build/test commands for root Gradle projects. - Added FastAPI route feature mapping and kept root/web Python project detection in sync. - Added Flask route feature mapping for Python projects, including `web/` source roots, common root entry files, non-list method literals, and Python framework detection. - Added Next.js route mapping for `src/app` and `src/pages` layouts, thanks @obatried. diff --git a/README.md b/README.md index 2334bef..c92c073 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ validation commands and records a patch attempt under `.clawpatch/`. - Next.js `app/` and `pages/` routes - Go package slices from `go list ./...`, including command packages - Go package tests and same-repo imports as review context +- Java/Kotlin Gradle source groups and root Gradle build/test commands - Rust `src/main.rs`, `src/bin/*.rs`, `src/lib.rs`, `crates/*`, and `tests/*.rs` - Python project metadata, console scripts, bounded source groups, pytest suites, diff --git a/docs/feature-mapping.md b/docs/feature-mapping.md index 1e97acb..e645ce7 100644 --- a/docs/feature-mapping.md +++ b/docs/feature-mapping.md @@ -39,7 +39,7 @@ Supported deterministic mappers today: - SwiftPM executable targets, library targets, and test suites - nested SwiftPM packages - Apple/Xcode projects from `project.yml`, `.xcodeproj`, or `.xcworkspace` -- Gradle/Android modules from `settings.gradle(.kts)` and `build.gradle(.kts)` +- Java/Kotlin Gradle modules from `settings.gradle(.kts)` and `build.gradle(.kts)` - common config files The mapper does not call a model. It uses repo conventions and cheap filesystem @@ -53,6 +53,7 @@ be found cheaply. Native app mappers use the same bounded grouping model. SwiftPM packages can be discovered below the repo root, Apple projects are grouped by Swift source area, and Gradle modules are grouped from `src/main`, `src/test`, and `src/androidTest`. +Root Gradle projects get default `gradle`/`./gradlew` build and test commands. Python mapping covers `pyproject.toml` metadata, `[project.scripts]` and `[tool.poetry.scripts]` console scripts, root app files, source groups under diff --git a/docs/quickstart.md b/docs/quickstart.md index 0c930eb..9ba6aa6 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -45,6 +45,7 @@ This discovers reviewable features: - npm package bins and scripts - Next.js routes - Go packages and commands +- Java/Kotlin Gradle modules - Python packages, console scripts, Flask routes, and pytest suites - Rust crates and binaries - SwiftPM targets and tests diff --git a/src/detect.ts b/src/detect.ts index 09ee5c1..aed426f 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -144,6 +144,12 @@ async function languageDefaultCommands( if (languages.includes("python")) { return pythonDefaultCommands(root); } + if ( + (languages.includes("java") || languages.includes("kotlin")) && + (await isRootGradleProject(root)) + ) { + return gradleDefaultCommands(root); + } return { typecheck: null, @@ -240,6 +246,25 @@ async function detectPackageManagers(root: string): Promise { const pythonPackageManagers = new Set(["uv", "poetry", "pdm", "hatch", "pip", "python"]); +async function isRootGradleProject(root: string): Promise { + return ( + (await pathExists(join(root, "settings.gradle"))) || + (await pathExists(join(root, "settings.gradle.kts"))) || + (await pathExists(join(root, "build.gradle"))) || + (await pathExists(join(root, "build.gradle.kts"))) + ); +} + +async function gradleDefaultCommands(root: string): Promise { + const runner = (await pathExists(join(root, "gradlew"))) ? "./gradlew" : "gradle"; + return { + typecheck: `${runner} build`, + lint: null, + format: null, + test: `${runner} test`, + }; +} + async function pythonDefaultCommands(root: string): Promise { const info = await pythonProjectInfo(root); const runner = await pythonRunner(root); @@ -725,6 +750,9 @@ async function detectLanguages(root: string): Promise { if (!languages.includes("python") && (await containsReviewablePythonFile(root))) { languages.push("python"); } + if (!languages.includes("java") && (await containsReviewableJavaFile(root))) { + languages.push("java"); + } if ( !languages.includes("swift") && ((await containsFileNamed(root, "Package.swift", 5)) || @@ -734,7 +762,8 @@ async function detectLanguages(root: string): Promise { } if ( !languages.includes("kotlin") && - ((await containsFileWithExtension(root, ".kt", 5)) || + ((await containsReviewableKotlinFile(root)) || + (await containsFileWithExtension(root, ".kt", 5)) || (await containsFileWithExtension(root, ".kts", 5))) ) { languages.push("kotlin"); @@ -742,6 +771,25 @@ async function detectLanguages(root: string): Promise { return languages; } +const jvmSourceSearchRoots = ["src", "app", "apps", "lib"] as const; + +async function containsReviewableJavaFile(root: string): Promise { + return containsReviewableJvmFile(root, ".java"); +} + +async function containsReviewableKotlinFile(root: string): Promise { + return containsReviewableJvmFile(root, ".kt"); +} + +async function containsReviewableJvmFile(root: string, extension: string): Promise { + for (const prefix of jvmSourceSearchRoots) { + if (await containsFileWithExtension(join(root, prefix), extension, 8)) { + return true; + } + } + return false; +} + async function isPythonProject(root: string): Promise { return ( (await pathExists(join(root, "pyproject.toml"))) || diff --git a/src/mapper.test.ts b/src/mapper.test.ts index 5f87c22..8df65a9 100644 --- a/src/mapper.test.ts +++ b/src/mapper.test.ts @@ -493,6 +493,55 @@ describe("mapFeatures", () => { expect(titles).toContain("Gradle source src"); expect(titles).toContain("Gradle test suite src"); expect(titles.some((title) => title.includes("./src"))).toBe(false); + expect(project.detected.languages).toContain("kotlin"); + expect(project.detected.commands).toMatchObject({ + typecheck: "gradle build", + test: "gradle test", + }); + }); + + it("detects Kotlin and Gradle commands for Groovy Gradle root projects", async () => { + const root = await fixtureRoot("clawpatch-root-kotlin-gradle-detect-"); + await writeFixture(root, "settings.gradle", "pluginManagement {}\n"); + await writeFixture(root, "build.gradle", "plugins { id 'org.jetbrains.kotlin.jvm' }\n"); + await writeFixture(root, "src/main/kotlin/com/example/app/App.kt", "class App\n"); + await writeFixture(root, "src/test/kotlin/com/example/app/AppTest.kt", "class AppTest\n"); + + const project = await detectProject(root); + + expect(project.detected.languages).toContain("kotlin"); + expect(project.detected.packageManagers).toContain("gradle"); + expect(project.detected.commands).toMatchObject({ + typecheck: "gradle build", + test: "gradle test", + }); + }); + + it("detects Java and wrapper Gradle commands for root Gradle projects", async () => { + const root = await fixtureRoot("clawpatch-root-java-gradle-detect-"); + await writeFixture(root, "gradlew", "#!/bin/sh\n"); + await writeFixture(root, "settings.gradle", "pluginManagement {}\n"); + await writeFixture(root, "build.gradle", "plugins { id 'java' }\n"); + await writeFixture(root, "src/main/java/com/example/App.java", "class App {}\n"); + await writeFixture(root, "src/test/java/com/example/AppTest.java", "class AppTest {}\n"); + + const project = await detectProject(root); + + expect(project.detected.languages).toContain("java"); + expect(project.detected.packageManagers).toContain("gradle"); + expect(project.detected.commands).toMatchObject({ + typecheck: "./gradlew build", + test: "./gradlew test", + }); + }); + + it("does not detect Java from documentation-only Java files", async () => { + const root = await fixtureRoot("clawpatch-docs-java-detect-"); + await writeFixture(root, "docs/Example.java", "class Example {}\n"); + + const project = await detectProject(root); + + expect(project.detected.languages).not.toContain("java"); }); it("maps build.gradle-only roots without empty Gradle groups", async () => { @@ -526,6 +575,8 @@ describe("mapFeatures", () => { const titles = result.features.map((feature) => feature.title); expect(project.detected.packageManagers).toContain("gradle"); + expect(project.detected.commands.typecheck).toBeNull(); + expect(project.detected.commands.test).toBeNull(); expect(titles).toContain("Gradle module apps/android"); expect(titles).toContain("Gradle source apps/android/src"); });