From 9edcd5affce28c5d6251cd6617e8faedc47c5794 Mon Sep 17 00:00:00 2001 From: Antonio Perez Dieppa Date: Tue, 2 Jun 2026 14:34:34 +0100 Subject: [PATCH] feat(kotlin): kapt opt-out and default-stage dedup across modules (#921) * feat(kotlin): kapt opt-out and default-stage dedup across modules Address the two Kotlin-support follow-ups captured after the #918 fix in KOTLIN_SUPPORT_FOLLOWUPS.md. * Kapt auto-apply opt-out via `flamingock.autoApplyKapt` Gradle property (default true; opt out with literal case-insensitive `false`). Gradle property, not DSL: the `plugins { }` block evaluates before `flamingock { }`, so a DSL setter would fire too late. * Mixed Java+Kotlin modules no longer produce duplicate default-stage entries in the composite pipeline. `MetadataLoader.CompositePipelineBuilder` now applies the same id-deduplicated same-name collapse to default stages that legacy stages already used. Identity-field mismatches still WARN for default stages so genuine misconfigurations stay visible. --- build.gradle.kts | 2 +- .../common/core/metadata/MetadataLoader.java | 87 +++++--- .../CompositePipelineBuilderTest.java | 202 ++++++++++++++++++ .../docs/KOTLIN_SUPPORT_FOLLOWUPS.md | 40 +++- .../io/flamingock/gradle/FlamingockPlugin.kt | 21 +- .../gradle/internal/FlamingockConstants.kt | 8 + .../flamingock/gradle/FlamingockPluginTest.kt | 53 +++++ 7 files changed, 370 insertions(+), 43 deletions(-) create mode 100644 core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/metadata/CompositePipelineBuilderTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 2e1620342..a95e21751 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ plugins { allprojects { group = "io.flamingock" - val declaredVersion = "1.4.1-SNAPSHOT" + val declaredVersion = "1.4.2-SNAPSHOT" version = VersionManager.resolveVersion(declaredVersion, project.hasProperty("release")) extra["generalUtilVersion"] = "1.5.3" diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java index 6f4aaded6..a6b8bb816 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/metadata/MetadataLoader.java @@ -182,10 +182,16 @@ FlamingockMetadata aggregate(List modules) { static final class CompositePipelineBuilder { PreviewPipeline buildFrom(List modules) { // Stage assembly preserves stage instances by reference — no defensive copies. - // Default stages are kept as-is (duplicate names across modules surface a warning - // because that's likely a configuration mistake). Legacy stages are merged - // group-by-name via collapseLegacyStagesByName; same-name legacy stages across - // modules are merged silently because that's the documented design. + // Stages with the same name across modules are collapsed group-by-name via + // collapseStagesByName for BOTH legacy and default stages. The collapse + // id-deduplicates the change set across the group; this is what makes the + // kapt + javac scenario in a mixed Java+Kotlin module produce one default stage + // with the union of changes rather than two side-by-side. Identity-field + // mismatches (different type / sourcesPackage / resourcesDir on stages that + // share a name) still surface a WARN for default stages so genuine multi-module + // misconfigurations stay visible; legacy stages stay silent because historical + // Mongock setups frequently have multiple modules legitimately producing the + // same legacy stage. List defaultStages = new ArrayList<>(); List legacyStages = new ArrayList<>(); SystemPreviewStage systemStage = null; @@ -209,22 +215,12 @@ PreviewPipeline buildFrom(List modules) { } } - // Warn on duplicate names among DEFAULT stages only — for LEGACY, collapsing - // same-name stages is the intended outcome and would otherwise surface false - // positives in any project with multiple Mongock-using modules. - Set seenDefaultNames = new HashSet<>(); - for (PreviewStage s : defaultStages) { - if (!seenDefaultNames.add(s.getName())) { - logger.warn("Duplicate stage name '{}' across modules — proceeding with both.", - s.getName()); - } - } - - List collapsedLegacy = collapseLegacyStagesByName(legacyStages); + List collapsedLegacy = collapseStagesByName(legacyStages); + List collapsedDefault = collapseStagesByName(defaultStages); List allStages = new ArrayList<>(); allStages.addAll(collapsedLegacy); - allStages.addAll(defaultStages); + allStages.addAll(collapsedDefault); // Surface the rare case where the system stage name clashes with any // resolved (post-legacy-collapse) stage name in the composite. @@ -269,54 +265,83 @@ private static SystemPreviewStage mergeSystem(SystemPreviewStage soFar, PreviewS } /** - * Group legacy stages by name and merge each group independently. Returns one - * {@link PreviewStage} per distinct legacy-stage name, in first-seen order across - * modules (deterministic for a given module discovery order). + * Group stages by name and merge each group independently. Returns one + * {@link PreviewStage} per distinct stage name, in first-seen order across modules + * (deterministic for a given module discovery order). * - *

Stages with different names stay separate — historically Mongock was the only - * producer (always {@code flamingock-legacy-stage}), but a future legacy source can - * declare its own stage name and is preserved as a peer alongside Mongock's. + *

Type-agnostic: handles both legacy and default stages with the same id-dedup + * semantics. Same-name groups across modules are merged silently for LEGACY stages + * (historical Mongock setups frequently have multiple modules producing the same + * legacy stage) and with an identity-field mismatch WARN for DEFAULT stages (so the + * "two modules declared 'mainStage' with different sourcesPackage" misconfiguration + * stays visible). */ - private static List collapseLegacyStagesByName(List legacyStages) { - if (legacyStages.isEmpty()) return new ArrayList<>(); + private static List collapseStagesByName(List stages) { + if (stages.isEmpty()) return new ArrayList<>(); LinkedHashMap> byName = new LinkedHashMap<>(); - for (PreviewStage s : legacyStages) { + for (PreviewStage s : stages) { byName.computeIfAbsent(s.getName(), k -> new ArrayList<>()).add(s); } List result = new ArrayList<>(byName.size()); for (List group : byName.values()) { - result.add(group.size() == 1 ? group.get(0) : mergeSameNameLegacyStages(group)); + result.add(group.size() == 1 ? group.get(0) : mergeSameNameStages(group)); } return result; } /** - * id-deduplicated union of changes for legacy stages that already share a name. The - * first stage's name/description/sourcesPackage/resourcesDir wins; subsequent stages + * id-deduplicated union of changes for stages that already share a name. The first + * stage's name/type/description/sourcesPackage/resourcesDir wins; subsequent stages * contribute only changes whose ids haven't been seen yet. + * + *

Emits a WARN on identity-field mismatch (different {@code type}, + * {@code sourcesPackage}, or {@code resourcesDir}) for DEFAULT stages — this is + * almost certainly a configuration mistake worth surfacing. LEGACY stages stay + * silent on mismatch by design: existing Mongock-using modules sometimes + * legitimately diverge on these fields without it being a real problem. */ - private static PreviewStage mergeSameNameLegacyStages(List sameName) { + private static PreviewStage mergeSameNameStages(List sameName) { PreviewStage first = sameName.get(0); List merged = new ArrayList<>(); Set seenIds = new HashSet<>(); if (first.getChanges() != null) { first.getChanges().forEach(c -> { merged.add(c); seenIds.add(c.getId()); }); } + boolean identityMismatch = false; for (int i = 1; i < sameName.size(); i++) { PreviewStage extra = sameName.get(i); + if (!identityMismatch && hasIdentityMismatch(first, extra)) { + identityMismatch = true; + } if (extra.getChanges() == null) continue; for (AbstractPreviewChange c : extra.getChanges()) { if (seenIds.add(c.getId())) { merged.add(c); } else { - logger.debug("Deduplicated legacy change id '{}' in stage '{}' (already contributed by an earlier module)", + logger.debug("Deduplicated change id '{}' in stage '{}' (already contributed by an earlier module)", c.getId(), first.getName()); } } } + if (identityMismatch && first.getType() != StageType.LEGACY) { + logger.warn("Stage '{}' is declared by multiple modules with mismatched identity (type / sourcesPackage / resourcesDir). " + + "Merging change sets; first-seen identity wins — verify your @EnableFlamingock declarations.", + first.getName()); + } return new PreviewStage(first.getName(), first.getType(), first.getDescription(), first.getSourcesPackage(), first.getResourcesDir(), merged); } + + /** + * Two same-name stages have a real configuration mismatch when their type, + * sourcesPackage, or resourcesDir differ. Description is excluded — it's free-text + * and can reasonably differ across modules without indicating a problem. + */ + private static boolean hasIdentityMismatch(PreviewStage a, PreviewStage b) { + return a.getType() != b.getType() + || !java.util.Objects.equals(a.getSourcesPackage(), b.getSourcesPackage()) + || !java.util.Objects.equals(a.getResourcesDir(), b.getResourcesDir()); + } } /** Union map; later modules win on key clash. */ diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/metadata/CompositePipelineBuilderTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/metadata/CompositePipelineBuilderTest.java new file mode 100644 index 000000000..e3083161b --- /dev/null +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/metadata/CompositePipelineBuilderTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.flamingock.internal.common.core.metadata; + +import io.flamingock.api.StageType; +import io.flamingock.internal.common.core.preview.AbstractPreviewChange; +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.flamingock.internal.common.core.preview.PreviewPipeline; +import io.flamingock.internal.common.core.preview.PreviewStage; +import io.flamingock.internal.common.core.preview.SystemPreviewStage; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Unit tests for {@code MetadataLoader.CompositePipelineBuilder}, focused on the + * stage-collapse logic that was generalised to cover default stages (in addition to legacy + * stages) so that mixed Java + Kotlin modules — where kapt and javac each produce their own + * metadata provider with the same default stages — converge on a single default stage in the + * composite instead of two side-by-side entries. + */ +class CompositePipelineBuilderTest { + + @Test + @DisplayName("same-name default stages across modules collapse into one with merged change list") + void defaultStagesWithSameNameAreCollapsed() { + // Mirrors the kapt + javac scenario: two providers, same default stage, disjoint + // changes (kapt contributed the Kotlin changes, javac contributed the Java ones). + FlamingockMetadata moduleKapt = metadataWithDefaultStage( + "main-stage", "com.example", changes("KotlinChangeA", "KotlinChangeB")); + FlamingockMetadata moduleJavac = metadataWithDefaultStage( + "main-stage", "com.example", changes("JavaChangeC")); + + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() + .buildFrom(Arrays.asList(moduleKapt, moduleJavac)); + + List stages = new ArrayList<>(composite.getStages()); + assertEquals(1, stages.size(), + "Two providers contributing the same default stage must yield exactly one stage in the composite"); + PreviewStage merged = stages.get(0); + assertEquals("main-stage", merged.getName()); + assertEquals(StageType.DEFAULT, merged.getType()); + + Set changeIds = changeIds(merged); + assertEquals(setOf("KotlinChangeA", "KotlinChangeB", "JavaChangeC"), changeIds, + "Merged stage must contain the id-union of both providers' changes"); + } + + @Test + @DisplayName("same-name default stages with different sourcesPackage still collapse (first identity wins)") + void defaultStagesWithMismatchedIdentityStillCollapse() { + // The mismatch case: two modules declared a default stage with the same name but + // different sourcesPackage. We still merge — the alternative (keep both stages) was + // worse for any real use case — and first-seen identity wins. The behaviour change + // also emits a WARN at runtime; we don't assert on logging here (no in-module log + // capture infrastructure) and rely on code review for the WARN content. + FlamingockMetadata moduleA = metadataWithDefaultStage( + "shared-name", "com.example.a", changes("A1")); + FlamingockMetadata moduleB = metadataWithDefaultStage( + "shared-name", "com.example.b", changes("B1")); + + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() + .buildFrom(Arrays.asList(moduleA, moduleB)); + + List stages = new ArrayList<>(composite.getStages()); + assertEquals(1, stages.size(), + "Mismatched-identity same-name stages must still collapse to a single stage"); + PreviewStage merged = stages.get(0); + assertEquals("com.example.a", merged.getSourcesPackage(), + "First-seen sourcesPackage must win on identity-mismatch"); + assertEquals(setOf("A1", "B1"), changeIds(merged), + "Change sets must still be merged despite the identity mismatch"); + } + + @Test + @DisplayName("legacy stage collapse behaviour is preserved by the refactor") + void legacyStagesStillCollapse() { + // Regression guard: the legacy-stage collapse used to live in + // mergeSameNameLegacyStages; after generalisation it routes through the same + // mergeSameNameStages path. Verify same-name legacy stages still merge with id-dedup. + FlamingockMetadata m1 = metadataWithStage(StageType.LEGACY, + "flamingock-legacy-stage", null, changes("L1", "L2")); + FlamingockMetadata m2 = metadataWithStage(StageType.LEGACY, + "flamingock-legacy-stage", null, changes("L2", "L3")); + + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() + .buildFrom(Arrays.asList(m1, m2)); + + List stages = new ArrayList<>(composite.getStages()); + assertEquals(1, stages.size()); + PreviewStage merged = stages.get(0); + assertEquals(StageType.LEGACY, merged.getType()); + assertEquals(setOf("L1", "L2", "L3"), changeIds(merged), + "Duplicate legacy change ids across modules must be id-deduplicated"); + } + + @Test + @DisplayName("system stage id-dedup behaviour is preserved (separate code path, regression guard)") + void systemStageDedupUnchanged() { + // The system-stage merger (CompositePipelineBuilder#mergeSystem) is a different path + // from collapseStagesByName and is unchanged by this refactor. Sanity-check it. + SystemPreviewStage sysA = new SystemPreviewStage("system-stage", "desc", null, null, + new ArrayList<>(changes("S1", "S2"))); + SystemPreviewStage sysB = new SystemPreviewStage("system-stage", "desc", null, null, + new ArrayList<>(changes("S2", "S3"))); + FlamingockMetadata m1 = new FlamingockMetadata(); + m1.setPipeline(new PreviewPipeline(sysA, Collections.emptyList())); + FlamingockMetadata m2 = new FlamingockMetadata(); + m2.setPipeline(new PreviewPipeline(sysB, Collections.emptyList())); + + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() + .buildFrom(Arrays.asList(m1, m2)); + + PreviewStage system = composite.getSystemStage(); + assertNotNull(system, "Composite must carry a system stage when contributors had one"); + assertEquals(setOf("S1", "S2", "S3"), changeIds(system), + "Duplicate system-stage change ids across modules must be id-deduplicated"); + } + + @Test + @DisplayName("default stages with distinct names remain distinct (sanity: not over-collapsing)") + void distinctNameDefaultStagesAreKeptSeparate() { + // Guard against the lazy implementation that would collapse everything into one + // group. Distinct names must stay distinct. + FlamingockMetadata m1 = metadataWithDefaultStage("alpha", "com.alpha", changes("a1")); + FlamingockMetadata m2 = metadataWithDefaultStage("beta", "com.beta", changes("b1")); + + PreviewPipeline composite = new MetadataLoader.CompositePipelineBuilder() + .buildFrom(Arrays.asList(m1, m2)); + + List stages = new ArrayList<>(composite.getStages()); + assertEquals(2, stages.size(), "Distinct-name stages must remain separate"); + Set names = stages.stream().map(PreviewStage::getName).collect(Collectors.toSet()); + assertEquals(setOf("alpha", "beta"), names); + } + + // ---------------------------------------------------------------------- + // Fixture helpers + // ---------------------------------------------------------------------- + + private static FlamingockMetadata metadataWithDefaultStage(String name, + String sourcesPackage, + List changes) { + return metadataWithStage(StageType.DEFAULT, name, sourcesPackage, changes); + } + + private static FlamingockMetadata metadataWithStage(StageType type, + String name, + String sourcesPackage, + List changes) { + PreviewStage stage = new PreviewStage(name, type, null, sourcesPackage, null, changes); + FlamingockMetadata md = new FlamingockMetadata(); + md.setPipeline(new PreviewPipeline(Collections.singletonList(stage))); + return md; + } + + private static List changes(String... ids) { + List result = new ArrayList<>(ids.length); + for (String id : ids) { + CodePreviewChange change = new CodePreviewChange(); + change.setId(id); + result.add(change); + } + return result; + } + + private static Set changeIds(PreviewStage stage) { + if (stage.getChanges() == null) return Collections.emptySet(); + Set ids = new LinkedHashSet<>(); + for (AbstractPreviewChange c : stage.getChanges()) { + ids.add(c.getId()); + } + return ids; + } + + private static Set setOf(String... values) { + return new LinkedHashSet<>(Arrays.asList(values)); + } +} diff --git a/flamingock-gradle-plugin/docs/KOTLIN_SUPPORT_FOLLOWUPS.md b/flamingock-gradle-plugin/docs/KOTLIN_SUPPORT_FOLLOWUPS.md index 42e316823..eeba5c833 100644 --- a/flamingock-gradle-plugin/docs/KOTLIN_SUPPORT_FOLLOWUPS.md +++ b/flamingock-gradle-plugin/docs/KOTLIN_SUPPORT_FOLLOWUPS.md @@ -1,18 +1,31 @@ # Kotlin support — deferred follow-ups -**Status:** deferred — captured for a follow-up PR. Surfaced during review of the +**Status:** Section 1(a) and Section 2 are **implemented** (1.4.2 release). Section 1(b) — the +KSP port of `flamingock-processor` — remains deferred and awaits a scoping issue before +implementation. The original analysis is preserved below as context for future work. + +Surfaced during review of the [#918](https://github.com/flamingock/flamingock-java/issues/918) Kotlin-support fix (auto-apply `org.jetbrains.kotlin.kapt` when Kotlin JVM is detected, register `flamingock-processor` under the `kapt` configuration, harden the reflective Kotlin-compile-task arg wiring). -Two related concerns are intentionally **not** addressed in the bug-fix PR so the patch release -stays focused. They are described here in enough detail that the next PR doesn't have to -re-derive the analysis. +Two related concerns were intentionally **not** addressed in the original bug-fix PR so the +patch release stayed focused. They are described here in enough detail that follow-up work +doesn't have to re-derive the analysis. --- ## 1. Kapt auto-apply: opt-out and KSP roadmap +**Status — item (a) opt-out: implemented in 1.4.2.** Exposed as the Gradle property +`flamingock.autoApplyKapt` (default `true`; opt out with literal case-insensitive `false`). +Implemented as a Gradle property rather than a `flamingock { … }` DSL setting because the +`plugins { }` block evaluates before the `flamingock { }` block — a DSL setter would always +fire too late to influence the auto-apply that already happened during `plugins { }`. See +`docs/get-started/gradle-plugin.md` (Kotlin support section) for the user-facing documentation. + +**Status — item (b) KSP port: still deferred.** Needs its own scoping issue. + ### Current behaviour `FlamingockPlugin.apply()` reacts to `org.jetbrains.kotlin.jvm` being present and @@ -76,6 +89,15 @@ should land first so the KSP work has somewhere to plug in. ## 2. Mixed Java+Kotlin modules: duplicate same-name default stages +**Status: implemented in 1.4.2 via the recommended merge-layer dedup.** +`MetadataLoader.CompositePipelineBuilder` now applies the same id-deduplicated same-name +collapse to **default** stages that has always been applied to **legacy** stages. The two +helpers were generalised into `collapseStagesByName` + `mergeSameNameStages`. Identity-field +mismatches (different `type` / `sourcesPackage` / `resourcesDir` on stages sharing a name) +still emit a WARN for default stages so genuine multi-module misconfigurations stay visible; +legacy stages remain silent on mismatch by design. The analysis below is preserved for +context. + ### Mechanism When a single module has **both** Java and Kotlin sources annotated with Flamingock, the @@ -172,9 +194,7 @@ and one in Kotlin, both under the same `@EnableFlamingock` stage. Assertions: ## Tracking -When the follow-up PR is filed, please open two issues referencing this document: - -1. **Kotlin annotation processing: add opt-out toggle and scope KSP port** — implements - Section 1's deferred items (a) and (b). -2. **MetadataLoader: dedupe same-name default stages across providers** — implements - Section 2's solution direction (a). +- Section 1(a) and Section 2 — implemented in 1.4.2. No further tracking issues needed. +- Section 1(b) — KSP port of `flamingock-processor`. Still requires a scoping issue before + implementation; not every `javax.annotation.processing` API has a direct KSP equivalent + and `RoundDiscovery` / the `Filer`-based incremental cache need substantive rework. diff --git a/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/FlamingockPlugin.kt b/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/FlamingockPlugin.kt index 406c0ef09..70e7f6c12 100644 --- a/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/FlamingockPlugin.kt +++ b/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/FlamingockPlugin.kt @@ -57,8 +57,14 @@ class FlamingockPlugin : Plugin { // Kotlin projects need KAPT so the Flamingock annotation processor runs on Kotlin // sources. Apply it automatically to preserve the plugin's zero-boilerplate intent. + // Users who want to manage kapt themselves (or who plan to migrate to KSP) can opt + // out via -Pflamingock.autoApplyKapt=false. The opt-out has to be a Gradle property + // and not a `flamingock { … }` DSL setting because the `plugins { }` block (where + // the user applies kotlin.jvm) evaluates before the `flamingock { }` block, so any + // DSL setter would fire too late to influence the auto-apply that already happened. project.plugins.withId(KOTLIN_JVM_PLUGIN_ID) { - if (!project.plugins.hasPlugin(FlamingockConstants.KAPT_PLUGIN_ID)) { + if (shouldAutoApplyKapt(project) + && !project.plugins.hasPlugin(FlamingockConstants.KAPT_PLUGIN_ID)) { project.pluginManager.apply(FlamingockConstants.KAPT_PLUGIN_ID) } } @@ -86,6 +92,19 @@ class FlamingockPlugin : Plugin { } } + /** + * Decides whether the plugin should auto-apply `org.jetbrains.kotlin.kapt`. Defaults to + * `true`; users opt out by setting [FlamingockConstants.AUTO_APPLY_KAPT_PROPERTY] to a + * value equal (case-insensitively) to `"false"`. Any other value — including unset — + * preserves the auto-apply behaviour. Read at `withId`-fire time so the decision is + * available before kapt would otherwise be applied. Visible to the same module so tests + * can exercise the property handling directly. + */ + internal fun shouldAutoApplyKapt(project: Project): Boolean { + val raw = project.findProperty(FlamingockConstants.AUTO_APPLY_KAPT_PROPERTY) ?: return true + return !raw.toString().equals("false", ignoreCase = true) + } + private fun validateConfiguration(extension: FlamingockExtension) { if (extension.isCommunityEnabled && extension.isCloudEnabled) { throw GradleException( diff --git a/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/internal/FlamingockConstants.kt b/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/internal/FlamingockConstants.kt index 861a559b7..58e229141 100644 --- a/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/internal/FlamingockConstants.kt +++ b/flamingock-gradle-plugin/src/main/kotlin/io/flamingock/gradle/internal/FlamingockConstants.kt @@ -28,6 +28,14 @@ internal object FlamingockConstants { const val KAPT_PLUGIN_ID = "org.jetbrains.kotlin.kapt" const val KSP_PLUGIN_ID = "com.google.devtools.ksp" + /** + * Gradle property that, when set to `false`, suppresses the auto-application of + * `org.jetbrains.kotlin.kapt` in Kotlin projects. Default behaviour (property unset or + * any value other than literal `false`, case-insensitive) is to auto-apply kapt so the + * Flamingock annotation processor runs against Kotlin sources. + */ + const val AUTO_APPLY_KAPT_PROPERTY = "flamingock.autoApplyKapt" + val FLAMINGOCK_VERSION: String by lazy { FlamingockConstants::class.java.classLoader .getResourceAsStream("flamingock-plugin.properties") diff --git a/flamingock-gradle-plugin/src/test/kotlin/io/flamingock/gradle/FlamingockPluginTest.kt b/flamingock-gradle-plugin/src/test/kotlin/io/flamingock/gradle/FlamingockPluginTest.kt index 963e355b3..7736c9838 100644 --- a/flamingock-gradle-plugin/src/test/kotlin/io/flamingock/gradle/FlamingockPluginTest.kt +++ b/flamingock-gradle-plugin/src/test/kotlin/io/flamingock/gradle/FlamingockPluginTest.kt @@ -110,6 +110,59 @@ class FlamingockPluginTest { ) } + @Test + fun `shouldAutoApplyKapt returns true when no property is set`() { + // The opt-out is read at withId-fire time via Gradle's findProperty. With no value + // anywhere (gradle.properties, -P, or env), the helper returns true so kapt is + // auto-applied — preserving the 1.4.1 default behaviour. + val project = ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build() + val plugin = FlamingockPlugin() + + assertTrue( + plugin.shouldAutoApplyKapt(project), + "Default (property unset) must auto-apply kapt" + ) + } + + @Test + fun `shouldAutoApplyKapt returns false when flamingock autoApplyKapt is set to false`() { + // Simulates a user setting flamingock.autoApplyKapt=false in gradle.properties or + // via -Pflamingock.autoApplyKapt=false. ProjectBuilder lets us seed extra properties + // via the project's ExtraPropertiesExtension; the same lookup path Gradle uses for + // -P / gradle.properties resolves through findProperty(). + val project = ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build() + project.extensions.extraProperties.set(FlamingockConstants.AUTO_APPLY_KAPT_PROPERTY, "false") + val plugin = FlamingockPlugin() + + assertFalse( + plugin.shouldAutoApplyKapt(project), + "Explicit 'false' must opt out of kapt auto-apply" + ) + } + + @Test + fun `shouldAutoApplyKapt treats false-string case-insensitively, other values default to true`() { + // Case variants of "false" all opt out; anything else (including "no", "0", "FALSE!", + // arbitrary strings) preserves the default. This avoids surprising the user with + // values that look like opt-out but aren't (e.g. "no" stays on so the user sees the + // auto-apply happen and learns the canonical property name from docs). + val plugin = FlamingockPlugin() + + listOf("false", "False", "FALSE").forEach { v -> + val p = ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build() + p.extensions.extraProperties.set(FlamingockConstants.AUTO_APPLY_KAPT_PROPERTY, v) + assertFalse(plugin.shouldAutoApplyKapt(p), "'$v' must opt out") + } + listOf("true", "True", "TRUE", "yes", "no", "0", "1", "").forEach { v -> + val p = ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build() + p.extensions.extraProperties.set(FlamingockConstants.AUTO_APPLY_KAPT_PROPERTY, v) + assertTrue( + plugin.shouldAutoApplyKapt(p), + "'$v' must not opt out — only literal case-insensitive 'false' suppresses auto-apply" + ) + } + } + @Test fun `applying the plugin creates the flamingock extension`() { val project = ProjectBuilder.builder().withProjectDir(projectDir.toFile()).build()