diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5a6ccf6..d3564e8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,5 +1,5 @@
# CI(docs/チェックリスト.md §7)。基本は ubuntu(ルート build / integration-test のモジュールビルド /
-# TestKit の分割実行)で、OS 分岐は必要な範囲のみ: macos = Apple ターゲットのネイティブツールチェーン確認
+# TestKit の分割実行 / Maven ビルド)で、OS 分岐は必要な範囲のみ: macos = Apple ターゲットのネイティブツールチェーン確認
# (ルート build のみ)・windows = mingwX64 の実行テストのみ(native の box 実行はホスト依存ゲートで
# Windows でしか走らない)。main への push では加えて依存グラフを登録する。
# アクションはコミットハッシュで固定する(末尾コメントの vX.Y.Z は参照用。更新は Dependabot が追随する)
@@ -67,8 +67,8 @@ jobs:
key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }}
- run: ./gradlew build
- # integration-test は 8 モジュールのビルドと TestKit テストを別ジョブへ分ける。
- # 同一ジョブだと両者がランナーのコアを奪い合い、TestKit の開始が待たされる
+ # integration-test は 9 モジュールのビルドと、重いテスト(TestKit / Maven)を別ジョブへ分ける。
+ # 同一ジョブだと互いにランナーのコアを奪い合い、開始が待たされる
integration-modules:
runs-on: ubuntu-latest
timeout-minutes: 90
@@ -83,7 +83,9 @@ jobs:
with:
path: ~/.konan
key: konan-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }}
- - run: ./gradlew -p integration-test build -x :gradle-integration:test
+ - run: >-
+ ./gradlew -p integration-test build
+ -x :gradle-integration:test -x :maven-integration:test
# TestKit テストはランナーを増やして分割する。ランナーのコア数ではジョブ内の並行度を上げても
# 実行時間が縮まらないため(同時実行 2 で 1 割・4 で頭打ち)、ジョブ内は直列にして分割数で稼ぐ。
@@ -112,6 +114,27 @@ jobs:
./gradlew -p integration-test :gradle-integration:test --rerun
-PtestShardIndex=${{ matrix.shard }} -PtestShardCount=3 -PtestKitParallelism=1
+ # Maven ビルドの検証(docs/test/ケース06-ビルド動態.md §7)。Maven 本体は Gradle 側の依存として
+ # 取得するためインストール手順は要らない。フィクスチャは Kotlin 一式をテスト専用のローカルリポジトリ
+ # (~/.m2/repository とは別。docs/test/フィクスチャ構成.md §6)へ取得するため、そこをキャッシュする
+ integration-maven:
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
+ with:
+ distribution: temurin
+ java-version: 17
+ - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
+ - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: integration-test/maven-integration/build/maven-local-repo
+ key: maven-repo-${{ runner.os }}-${{ hashFiles('gradle/libs.versions.toml') }}
+ # フィクスチャの入力にはローカル Maven の非タイムスタンプ SNAPSHOT が含まれ UP-TO-DATE 判定が
+ # 信用できないため --rerun で必ず再実行する(TestKit と同じ理由)
+ - run: ./gradlew -p integration-test :maven-integration:test --rerun
+
build-macos:
runs-on: macos-latest
timeout-minutes: 60
diff --git a/README.md b/README.md
index b4ae007..1cec136 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@ operational API of enums on top.
## Setup
+### Gradle
+
```kotlin
plugins {
kotlin("multiplatform") version "2.4.10" // or kotlin("jvm") — any Kotlin target plugin
@@ -38,6 +40,54 @@ sealedClassEnumizer {
}
```
+### Maven
+
+Declare the plugin as a dependency of kotlin-maven-plugin and name it in `compilerPlugins`; the
+compiler plugin follows as a transitive dependency. It applies to every kotlin-maven-plugin
+execution, production and test compilations alike.
+
+```xml
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ sealed-class-enumizer
+
+
+
+
+ io.github.projectmapk
+ sealed-class-enumizer-maven-plugin
+ 2.4.10-0.1.0
+
+
+
+```
+
+Unlike the Gradle plugin, the runtime API is not added for you — declare it as a project
+dependency (Maven resolves the platform artifact, hence the `-jvm` suffix):
+
+```xml
+
+ io.github.projectmapk
+ sealed-class-enumizer-runtime-api-jvm
+ 2.4.10-0.1.0
+
+```
+
+The project-wide default label case is a property:
+
+```xml
+
+ AS_DECLARED
+
+```
+
+The same option can be given through kotlin-maven-plugin's `pluginOptions`
+(`sealed-class-enumizer:labelCase=...`), which takes precedence over the property.
+
### IntelliJ IDEA
IntelliJ's K2 mode does not load third-party compiler plugins by default, so generated
@@ -161,7 +211,8 @@ Labels default to the leaf's simple name and resolve with this priority:
1. `@EnumishLabel("...")` on the leaf — used as-is (no case conversion); for resolving label
clashes and keeping persisted labels stable across renames
2. `@Enumize(labelCase = ...)` on the hierarchy
-3. The project-wide default from the Gradle DSL (`sealedClassEnumizer { labelCase = ... }`)
+3. The project-wide default from the build — the Gradle DSL `sealedClassEnumizer { labelCase }`
+ or the Maven `sealed-class-enumizer.labelCase` property
4. `AS_DECLARED` (no conversion)
Available cases: `AS_DECLARED`, `UPPER_SNAKE_CASE`, `SNAKE_CASE`, `KEBAB_CASE`.
@@ -176,7 +227,7 @@ Label uniqueness within a hierarchy is enforced at compile time.
|---|---|
| Kotlin | 2.4.x, K2 only. The plugin version encodes the Kotlin minor it targets; applying it to another Kotlin minor produces a build warning |
| Version format | `-`, e.g. `2.4.10-0.1.0` |
-| Build environment | Gradle 9+ / JDK 17+ (verified with Gradle 9.6) |
+| Build environment | Gradle 9+ or Maven 3.9+ / JDK 17+ (verified with Gradle 9.6 and Maven 3.9) |
| runtime-api (JVM) | Java 8+ bytecode |
| runtime-api targets | `jvm`, `js`, `wasmJs`, `wasmWasi`, `linuxX64`, `mingwX64`, `macosX64`, `macosArm64`, `iosArm64`, `iosSimulatorArm64`, `iosX64` |
@@ -192,8 +243,8 @@ Label uniqueness within a hierarchy is enforced at compile time.
## Versioning and support policy
- Versions follow `-`.
- All published artifacts (runtime API, compiler plugin, Gradle plugin and its marker) share the
- same version.
+ All published artifacts (runtime API, compiler plugin, Gradle plugin and its marker, Maven
+ plugin) share the same version.
- The Kotlin compiler plugin API has no stability guarantee, so each release targets exactly one
Kotlin minor; support covers the latest stable Kotlin minor including its patch releases.
- New Kotlin releases are followed by new plugin releases; older minors receive no backports.
diff --git a/build.gradle.kts b/build.gradle.kts
index 88dce66..13a9c22 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -40,8 +40,9 @@ allprojects {
ktfmt { kotlinLangStyle() }
}
-// API ドキュメント(GitHub Pages 掲載用)の集約。対象は公開 API を持つ 2 モジュールで、
-// compiler-plugin は内部実装のため含めない。集約出力は build/dokka/html
+// API ドキュメント(GitHub Pages 掲載用)の集約。対象は利用者がコードから触る公開 API を持つ 2 モジュールで、
+// compiler-plugin(内部実装)と maven-plugin(利用面は POM の設定であり Kotlin API ではない)は含めない。
+// 集約出力は build/dokka/html
dependencies {
dokka(project(":sealed-class-enumizer-runtime-api"))
dokka(project(":sealed-class-enumizer-gradle-plugin"))
diff --git "a/docs/test/\343\202\261\343\203\274\343\202\27106-\343\203\223\343\203\253\343\203\211\345\213\225\346\205\213.md" "b/docs/test/\343\202\261\343\203\274\343\202\27106-\343\203\223\343\203\253\343\203\211\345\213\225\346\205\213.md"
index 32f147b..fc6a32a 100644
--- "a/docs/test/\343\202\261\343\203\274\343\202\27106-\343\203\223\343\203\253\343\203\211\345\213\225\346\205\213.md"
+++ "b/docs/test/\343\202\261\343\203\274\343\202\27106-\343\203\223\343\203\253\343\203\211\345\213\225\346\205\213.md"
@@ -1,8 +1,9 @@
# ケース06 ビルド動態
-本資料は K14(ビルド動態)× O2/O5 の正典である。
+本資料は K14(ビルド動態)× K17(ビルドツール)× O2/O5 の正典である。
編集ケース #1〜#12(7-b/7-c 含む)・@Enumize 付与除去・決定性・基底不在ラウンド・
-空⇔非空遷移・跨 module ABI 伝播・旧バイナリ差し替え・runtime-api 公開・MPP klib ラウンドを担う。
+空⇔非空遷移・跨 module ABI 伝播・旧バイナリ差し替え・runtime-api 公開・MPP klib ラウンド・
+Maven ビルドを担う。
実行時 OUT 照合(O1)は clean 一致判定の手段として併用する。
他資料の IC・決定性に関する観測は本資料への参照で足りる(テスト戦略 §3 C4)。
@@ -83,3 +84,16 @@
| ID | 次元/値 | 観測と期待 | 実装 | 備考(関連V/診断) |
|---|---|---|---|---|
| BLD-48 | K14/K16=プロジェクト既定の伝達 | DSL `sealedClassEnumizer { labelCase = SNAKE_CASE }` がコンパイラオプション(labelCase。初のオプション経路)として届き、labelCase 未指定の階層へ適用される・@Enumize の具体指定(KEBAB_CASE)は DSL に勝つ。検証は label-case フィクスチャ内 JUnit(test タスク)の成功で行う | LabelCaseOptionTest#dslDefaultPropagatesToCompilerOption | convention 側(DSL 未設定 = AS_DECLARED)はケース01 API-55 |
+
+## 7. Maven ビルド
+
+Gradle 以外のビルドツール上での適用を担う(実 Maven ビルドで駆動。フィクスチャは
+[フィクスチャ構成](フィクスチャ構成.md) §6)。
+
+| ID | 次元/値 | 観測と期待 | 実装 | 備考(関連V/診断) |
+|---|---|---|---|---|
+| BLD-49 | K17=適用範囲 | `` へ記載すると kotlin-maven-plugin の compile / test-compile の双方へ適用され(適用ログ 2 件)、main / test 双方の階層で生成 API が解決・実行できる。label はプロジェクト既定を置かない構成で組み込み既定(AS_DECLARED) | MavenBuildTest#appliesToProductionAndTestCompilations | コンパイラプラグイン本体は maven-plugin の推移依存として載る・runtime-api は利用側宣言(概要 §7) |
+| BLD-50 | K17/K16=プロジェクト既定の伝達 | プロパティ `sealed-class-enumizer.labelCase` の値がコンパイラオプションとして届き、labelCase 未指定の階層へ適用される・@Enumize の具体指定はプロジェクト既定に勝つ | MavenBuildTest#projectDefaultLabelCaseReachesTheCompiler | Gradle 側の対応は BLD-48 |
+| BLD-51 | K17/K16=実行単位指定の優先 | `` の `sealed-class-enumizer:labelCase=...` がプロジェクト既定に優先する(拡張側が渡さないことで成立する。コンパイラのオプションは複数指定を受け付けない) | MavenBuildTest#executionLevelPluginOptionOverridesTheProjectDefault | 二重指定はコンパイラ側の失敗になるため、優先関係は拡張の抑止で実現する |
+| BLD-52 | K17×O2=診断のビルド失敗 | 非 sealed 宣言への `@Enumize` が ENUMIZE_NOT_SEALED としてコンパイルを失敗させ、Maven ビルドが失敗する | MavenBuildTest#compilerDiagnosticFailsTheBuild | 診断内容の正典はケース04 |
+| BLD-53 | K17=設定値の不正 | labelCase プロパティの不正値は、受け付ける値の一覧を添えたビルド失敗になる(コンパイラへ渡る前に設定面の名前を示す) | MavenBuildTest#unknownLabelCasePropertyFailsTheBuild | — |
diff --git "a/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md" "b/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md"
index 4977556..2cc16c8 100644
--- "a/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md"
+++ "b/docs/test/\343\203\206\343\202\271\343\203\210\346\210\246\347\225\245.md"
@@ -8,10 +8,10 @@
## 1. 検証対象と非対象
-- 検証対象は CLI / Gradle ビルドの成否・診断・生成物・実行時挙動に限る。
+- 検証対象は CLI / Gradle / Maven ビルドの成否・診断・生成物・実行時挙動に限る。
IDE(K2) はサードパーティ FIR プラグインを既定でロードしない(KTIJ-29248)ため、
IDE / Analysis API 側は検証対象外とする(既定の IDE 体験は概要 §6・設計00 §11 が既知の制限として持つ)。
-- 環境: Kotlin 2.4.10 / Gradle 9.6.1 / JVM toolchain 17 / `group=io.github.projectmapk`。
+- 環境: Kotlin 2.4.10 / Gradle 9.6.1 / Maven 3.9.12 / JVM toolchain 17 / `group=io.github.projectmapk`。
- 比較規約: 値の比較はなるべく観測用 data class を組み立て 1 回の `assertEquals` で行う。
kind の同一性は参照比較(`===`)で行う。
@@ -38,6 +38,7 @@ Kotlin 言語仕様がこのプラグインに対して張る空間を次の次
| K14 | ビルド動態 | clean / 編集ケース #1〜#12 / @Enumize 付与・除去 / 決定性 4 形態+ロケール / 基底不在ラウンド / 空⇔非空遷移 |
| K15 | 実行時状態 | 遅延初期化 / 初期化子再入 / マルチスレッド初回 / boxing / SAM 変換 / 多段サブタイプ / 無名 object・local クラスによる末端実装 |
| K16 | label 決定 | labelCase(未指定 / PROJECT_DEFAULT 明示 / 具体 4 種)× 明示 @EnumishLabel(無 / 有 / 空白 / 不正な付与先 4 形 = 基底・中間 sealed・階層外・kind companion)× プロジェクト既定(DSL 指定 / convention / 組み込み既定)‖ 衝突(変換由来・明示由来)・enum 末端 / companion 末端への一律適用・data object toString の乖離 |
+| K17 | ビルドツール | Gradle(`KotlinCompilerPluginSupportPlugin`)/ Maven(kotlin-maven-plugin 拡張)‖ 各ツールの設定面: 適用範囲(production / test)・プロジェクト既定の label ケース・実行単位のオプション指定・runtime-api の供給(Gradle=自動 / Maven=利用側宣言)・設定値の不正 |
観測面(1 ケースは 1 つ以上の観測面を持つ):
@@ -67,9 +68,11 @@ Kotlin 言語仕様がこのプラグインに対して張る空間を次の次
## 4. テストモジュール構成
-integration-test は親ビルドを composite 参照する独立 Gradle ビルドであり、8 モジュールから成る。
+integration-test は親ビルドを composite 参照する独立 Gradle ビルドであり、9 モジュールから成る。
「コンパイル成功で証明できる正値」は通常モジュール、「コンパイル失敗・2 回ビルド差分・バイト一致・別版組合せ」は
-TestKit(gradle-integration)に置く。
+TestKit(gradle-integration)に置く。
+Gradle 以外のビルドツール上の適用は専用モジュール(maven-integration)が担い、
+ツール本体を配布物として取得して実ビルドを駆動する(ホストへのインストールを前提にしない)。
| モジュール | プラグイン | 役割 |
|---|---|---|
@@ -81,6 +84,7 @@ TestKit(gradle-integration)に置く。
| mpp-producer | 適用 | MPP 生成側。common フィクスチャ+全 5 ターゲット box・klib/metadata(V5) |
| mpp-consumer | 未適用 | MPP 純消費。common からの生成 API 参照・跨 module×MPP の when |
| gradle-integration | TestKit | 診断(負値・near-miss)・IC 回帰・決定性・ABI 伝播・旧バイナリ・基底不在ラウンド |
+| maven-integration | Maven 駆動 | kotlin-maven-plugin 拡張としての適用範囲・label ケースの伝達と優先・診断と設定不正のビルド失敗(K17) |
## 5. 状態と ID 規約
diff --git "a/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md" "b/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md"
index 253d45f..5b74cca 100644
--- "a/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md"
+++ "b/docs/test/\343\203\225\343\202\243\343\202\257\343\202\271\343\203\201\343\203\243\346\247\213\346\210\220.md"
@@ -124,3 +124,32 @@ src/test には test compilation 配線検証用の小さな @Enumize 階層(T
- 順序フィクスチャの期待値は FQN の UTF-16 序数から機械的に導出し、期待列をコメントで併記する。
- producer-jvm のフィクスチャで「1 ファイル 1 クラス」規約より配置制約(同一ファイル要求・
複数トップレベル宣言)が優先される場合は、ファイル先頭コメントで適用外を明示する。
+
+## 6. maven-integration(Maven フィクスチャ)
+
+フィクスチャは 1 プロジェクト = 1 POM の合成 Maven プロジェクトであり、実 Maven ビルドで駆動する。
+
+| フィクスチャ | 構成 | 駆動内容 |
+|---|---|---|
+| basic | main = labelCase 未指定階層 + 具体指定階層(KEBAB_CASE)・test = 独自階層 + JUnit ‖ プロファイル 2 種(プロジェクト既定のプロパティ・実行単位の `` 指定) | 適用範囲(production / test の両コンパイレーション)と label ケースの決定(組み込み既定 / プロジェクト既定 / 実行単位指定 / 階層個別の具体指定)を実行時 OUT 行で照合 |
+| diag | 非 sealed 宣言への `@Enumize` ‖ プロファイル(labelCase の不正値) | 診断と設定不正値がビルド失敗として表面化することの照合 |
+
+運用方針:
+
+- プラグイン注入は local-repo 経路(前段で `publishAllToMavenLocal`)。
+ ただし Maven のローカルリポジトリは**テスト専用の場所へ隔離**し、本プロジェクトの成果物だけを
+ そこへ複製して解決する(複製は MavenHarness)。
+ 既定の `~/.m2/repository` を使うと、Maven が取得する Gradle module metadata を伴わない成果物が
+ そこへ入り、`mavenLocal()` を宣言する TestKit フィクスチャの variant 解決(capability による
+ `kotlin-test` の選択など)を壊すためである。
+- フィクスチャは版を直書きせず、`%%ENUMIZER_VERSION%%` / `%%KOTLIN_VERSION%%` を展開時に置換する
+ (値の正は親ビルド。展開は MavenHarness が行う)。
+- Maven 本体は配布物(`org.apache.maven:apache-maven`)をビルドの依存として取得し、
+ ホストへのインストールを前提にしない。
+ 起動は `bin` のスクリプトではなく classworlds の Launcher を直接叩く(zip
+ 展開では実行権限が落ちるため)。JVM はテストと同じ toolchain を使う。
+- 実行は直列とする(Maven のローカルリポジトリは共有資源であり、並行ダウンロードでの破損を避ける)。
+- 展開先は実行前に回収する(§4 と同じ)。
+- 検証手段は 2 種: ビルドの成否と出力照合(適用ログ・診断文言)・フィクスチャ内 JUnit が出す OUT 行。
+ 設定で変わる値(label)は OUT 行で照合し、設定に依らない不変はフィクスチャ内で表明する。
+- runtime-api は Maven では自動追加されないため(概要 §7)、フィクスチャが依存として明示宣言する。
diff --git "a/docs/test/\344\277\235\347\225\231.md" "b/docs/test/\344\277\235\347\225\231.md"
index e64cfd1..35623c0 100644
--- "a/docs/test/\344\277\235\347\225\231.md"
+++ "b/docs/test/\344\277\235\347\225\231.md"
@@ -14,7 +14,7 @@
| PEN-01 | (欠番) | IDE / Analysis API は検証対象外と判断(既定の IDE 体験は概要 §6・設計00 §11 が既知の制限として持つ) | — | — |
| PEN-02 | (欠番) | V1-(e) は現行挙動で仕様確定(概要 §8 に明文化・ManualImplTest / CrossModuleWhenTest / CrossModuleNegativeTest が固定済み) | — | — |
| PEN-03 | (欠番) | V7 は BLD-45 で実測確定 | — | — |
-| PEN-04 | V8 環境マトリクス | 階層不変条件は JVM+Gradle 実測のみ(JPS / Maven / JS・Native klib IC / Native クロスホスト / Android+R8 / Native リリース DCE / js・wasm の明示 DCE リンク経路 / klib の旧バイナリ差し替えは未検証。MPP の from-cache/relocated 一致は BLD-44 が担う) | 各環境の IC 共連れ実測・Native 決定性・release/明示 DCE リンクでの $EntriesHolder 保持・klib 差し替えの実行時解決 | 各環境の駆動ハーネス整備(不成立環境はサポート対象外と明記して解除) |
+| PEN-04 | V8 環境マトリクス | 階層不変条件は JVM+Gradle 実測のみ(JPS / Maven / JS・Native klib IC / Native クロスホスト / Android+R8 / Native リリース DCE / js・wasm の明示 DCE リンク経路 / klib の旧バイナリ差し替えは未検証。MPP の from-cache/relocated 一致は BLD-44 が担う。Maven は適用・オプション伝達・診断を BLD-49〜53 で実測済みで、残るのは IC 面のみ) | 各環境の IC 共連れ実測・Native 決定性・release/明示 DCE リンクでの $EntriesHolder 保持・klib 差し替えの実行時解決 | 各環境の駆動ハーネス整備(不成立環境はサポート対象外と明記して解除) |
| PEN-05 | (欠番) | E-1 成立モードは将来拡張として PEN-07 へ統合(v1 の全構成 MISMATCH は DIA-49 が固定) | — | — |
| PEN-06 | (欠番) | 縮退モード駆動は縮退未発生のため対象外と判断(現実化すれば成立側テスト群の失敗として検知される) | — | — |
| PEN-07 | 将来拡張(拡張時検証) | v1 非対応の拡張群: 追加基底 interface 注入・手動 `Enumized` の K 採用(E-1。位相リスクの代替=@Enumize の KClass パラメータ案)。検証は各拡張の着手時に行う | 追加基底一括注入・K 採用の成立側(E-1 は位相スパイクから始める) | 各拡張の着手(仕様確定→検証→ケース資料へ展開。K 採用は位相不成立確定なら MISMATCH 維持で本行から除く) |
diff --git "a/docs/\345\256\237\350\243\205\343\203\216\343\203\274\343\203\210.md" "b/docs/\345\256\237\350\243\205\343\203\216\343\203\274\343\203\210.md"
index 844d352..464803b 100644
--- "a/docs/\345\256\237\350\243\205\343\203\216\343\203\274\343\203\210.md"
+++ "b/docs/\345\256\237\350\243\205\343\203\216\343\203\274\343\203\210.md"
@@ -11,11 +11,14 @@
- `runtime-api`(MPP: jvm / js / wasmJs / wasmWasi / linuxX64 / mingwX64 / macosX64 /
macosArm64 / iosArm64 / iosSimulatorArm64 / iosX64)・`compiler-plugin`・`gradle-plugin`(plugin id
- `io.github.projectmapk.sealed-class-enumizer`)。
+ `io.github.projectmapk.sealed-class-enumizer`)・`maven-plugin`(kotlin-maven-plugin の拡張。
+ 利用側が `` へ書く名前は `sealed-class-enumizer`)。
パッケージルートは `io.github.projectmapk.sealedClassEnumizer`(group=io.github.projectmapk に統一)。
-- `compiler-plugin` の単体テストは名前定数の一致検証とケース変換の期待値固定のみで、
- 生成挙動の検証は独立 composite build の `integration-test`([テスト戦略](test/テスト戦略.md) §4 の 8
- モジュール)が持つ。
+- コンパイラプラグイン ID はビルドの単一情報源(ルート `gradle.properties`)から両プラグインの
+ Coordinates を生成する。Gradle の plugin id もこれに揃える。
+- `compiler-plugin` / `gradle-plugin` / `maven-plugin` の単体テストは定数と登録の一致検証や
+ 期待値固定に限り、生成挙動とビルドツール上の適用の検証は独立 composite build の
+ `integration-test`([テスト戦略](test/テスト戦略.md) §4 の 9 モジュール)が持つ。
- 仕様と実挙動の乖離は[修正方針案](修正方針案.md)が持つ。
該当テストは仕様どおりのアサートのまま `@Ignore` / `@Disabled` とする。
diff --git "a/docs/\346\246\202\350\246\201.md" "b/docs/\346\246\202\350\246\201.md"
index 5960cd4..50e4e1b 100644
--- "a/docs/\346\246\202\350\246\201.md"
+++ "b/docs/\346\246\202\350\246\201.md"
@@ -507,6 +507,7 @@ kind の `toString` は label getter 経由のため自動で追随するが、d
| `runtime-api` | `@Enumize`・label カスタマイズ(`@EnumishLabel` / `LabelCase`。§4)・基底型(`Enumish` / `EnumishCompanion` / `Enumized`)・`EntriesHolder` の基底クラス(`EnumishEntriesHolder`)・拡張プロパティ(`Enumized.label`)(§2。MPP・reflection 非依存) |
| `compiler-plugin` | K2 コンパイラプラグイン本体(FIR + IR)。オプション `labelCase`(プロジェクト既定の label ケース。具体ケースのみ受け付け、欠落時は AS_DECLARED。§4) |
| `gradle-plugin` | `KotlinCompilerPluginSupportPlugin`。全コンパイレーション(main / test / metadata / 全ターゲット)へ適用し、`runtime-api` を自動追加(production = main / metadata は生成 API が supertype として runtime-api の型を公開する ABI 依存のため **api スコープ**、test コンパイレーションは公開面を持たないため implementation。オプションで無効化可)。DSL の `labelCase`(convention = AS_DECLARED)を常に具体値でコンパイラオプションへ伝達する(§4) |
+| `maven-plugin` | `KotlinMavenPluginExtension`(kotlin-maven-plugin の拡張であり Mojo ではない)。`` へ記載したコンパイレーション(production / test)へ適用し、コンパイラプラグイン本体を推移依存として配る。プロジェクト既定の label ケースはプロパティ `sealed-class-enumizer.labelCase` から常に具体値でコンパイラオプションへ伝達する(§4。実行単位の `` に同じ指定がある場合はそちらを優先し、オプションの二重指定を避けるため何も渡さない)。`runtime-api` の自動追加は行わず、利用側が依存として宣言する(Maven では拡張からプロジェクト依存を追加できず、追加にはビルド拡張の別途宣言を要するため) |
- **K2 のみ**サポート。
実用下限は **Kotlin 2.4**(生成クラス内ネストの解決に関する Analysis API 側の不具合 KT-84625 が 2.4.0-Beta2
@@ -514,10 +515,11 @@ kind の `toString` は label getter 経由のため自動で追随するが、d
- コンパイラプラグイン API は「リリース毎に破壊的変更が入る」ことが公式に宣言されているた
め、**Kotlin のマイナーバージョン毎にアーティファクトを分割**して配布する。
分割は版番号で実現し、版形式は `-<自版>`(例: `2.4.10-0.1.0`)。
- 3 モジュールと plugin marker は常に同版で公開し、公開 artifactId は `sealed-class-enumizer-` 接頭辞付き
+ 4 モジュールと plugin marker は常に同版で公開し、公開 artifactId は `sealed-class-enumizer-` 接頭辞付き
(group `io.github.projectmapk` の直下に汎用名を置かない)。
サポートは最新の安定 Kotlin マイナー(とそのパッチ)のみで、旧マイナーへのバックポートは行わない。
- 対応外の Kotlin への適用は gradle-plugin が警告する(エラーにはしない)
+ 対応外の Kotlin への適用は gradle-plugin / maven-plugin が警告する(エラーにはしない。適用先の Kotlin
+ 版は、Gradle では KGP の版・Maven では kotlin-maven-plugin の版として読む)
- 階層(sealed 基底から末端までの範囲)は言語仕様上、同一パッケージ・同一モジュール・
(MPP では)同一ソースセットに閉じる。
末端が非 final な場合、そのサブタイプは階層の外側にあってよい(別モジュールでもよい。§3)。
@@ -619,7 +621,7 @@ klib(JS / Native / Wasm)への直列化 — IR-only 生成物(EntriesHolde
| 範囲 | 内容 |
|---|---|
-| IC の環境マトリクス | 階層不変条件(sealed 階層の一括再コンパイル)が JVM + Gradle 以外(JPS / Maven)でも成り立つこと(MPP は Kotlin 側が既定で非 incremental のため、この条件を要さないことを実測済み)。未検証の環境はサポート対象外と明記する |
+| IC の環境マトリクス | 階層不変条件(sealed 階層の一括再コンパイル)が JVM + Gradle 以外(JPS / Maven)でも成り立つこと(MPP は Kotlin 側が既定で非 incremental のため、この条件を要さないことを実測済み)。Maven はプラグインの適用・オプション伝達・診断を実測済みであり、未検証なのは IC 面のみである。未検証の環境はサポート対象外と明記する |
IDE(Analysis API)での生成 API の解決と kind 単位の網羅 when の判定は、未検証のまま**検証対象外**と判断済みである
(§6 の既定体験を既知の制限として受容する)。
diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts
index 6159c3a..e2efef5 100644
--- a/gradle-plugin/build.gradle.kts
+++ b/gradle-plugin/build.gradle.kts
@@ -20,8 +20,8 @@ dependencies {
tasks.test { useJUnitPlatform() }
-// plugin id の正定義。compiler-plugin の EnumizeCommandLineProcessor.PLUGIN_ID と一致していなければならない
-val pluginId = "io.github.projectmapk.sealed-class-enumizer"
+// plugin id はコンパイラプラグイン ID と同一とする(値の正はルートの gradle.properties)
+val pluginId = providers.gradleProperty("compilerPluginId").get()
gradlePlugin {
website = providers.gradleProperty("POM_URL")
diff --git a/gradle.properties b/gradle.properties
index 328b50f..2dd34fb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,6 +4,11 @@ kotlin.code.style=official
# (ルートの build.gradle.kts が結合して全モジュールへ配る)
enumizerVersion=0.1.0-SNAPSHOT
+# コンパイラプラグイン ID の単一情報源。compiler-plugin の EnumizeCommandLineProcessor.PLUGIN_ID と
+# 一致していなければならない。gradle-plugin / maven-plugin はビルド時生成の Coordinates 経由で参照し、
+# Gradle の plugin id もこれに揃える(利用者が書く識別子を 1 つに保つ)
+compilerPluginId=io.github.projectmapk.sealed-class-enumizer
+
# Maven Central 公開の POM 共通値(vanniktech maven publish の規約キー)。
# モジュール別の POM_NAME / POM_DESCRIPTION は各モジュールの gradle.properties が持つ
POM_URL=https://github.com/ProjectMapK/sealed-class-enumizer
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c4335d4..4ec642a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,7 @@
[versions]
kotlin = "2.4.10"
+# maven-plugin がコンパイル時に参照する Maven API の版(kotlin-maven-plugin 自身が依存する版に揃える)
+maven = "3.9.12"
autoservice = "0.1.5"
ktfmt = "0.26.0"
mavenPublish = "0.37.0"
@@ -9,6 +11,8 @@ dokka = "2.2.0"
[libraries]
kotlin-compiler-embeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
kotlin-gradle-plugin-api = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "kotlin" }
+kotlin-maven-plugin = { module = "org.jetbrains.kotlin:kotlin-maven-plugin", version.ref = "kotlin" }
+maven-core = { module = "org.apache.maven:maven-core", version.ref = "maven" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
diff --git a/integration-test/maven-integration/build.gradle.kts b/integration-test/maven-integration/build.gradle.kts
new file mode 100644
index 0000000..f39ade7
--- /dev/null
+++ b/integration-test/maven-integration/build.gradle.kts
@@ -0,0 +1,87 @@
+import java.util.Properties
+
+// Maven ビルドホスト(docs/test/テスト戦略.md §4)。
+// resources/fixtures 配下の合成 Maven プロジェクトを実 Maven で駆動し、kotlin-maven-plugin の拡張
+// としての適用(production / test 双方)・label ケースの伝達と優先順位・診断によるビルド失敗を検証する
+plugins { kotlin("jvm") }
+
+group = "io.github.projectmapk"
+
+version = "1.0-SNAPSHOT"
+
+kotlin { jvmToolchain(17) }
+
+dependencies { testImplementation(kotlin("test")) }
+
+// Maven 本体はビルドの依存として取得する(ホストへの Maven インストールを前提にしない)。
+// 版は親ビルドの version catalog を正とし、maven-plugin がコンパイル時に参照する Maven API と揃える
+val mavenVersion: String = libs.versions.maven.get()
+
+val mavenDistribution = configurations.dependencyScope("mavenDistribution")
+
+val mavenDistributionFiles =
+ configurations.resolvable("mavenDistributionFiles") { extendsFrom(mavenDistribution.get()) }
+
+dependencies { add(mavenDistribution.name, "org.apache.maven:apache-maven:$mavenVersion:bin@zip") }
+
+val mavenDistRoot = layout.buildDirectory.dir("maven-dist")
+
+// 配布物の展開。起動は bin/mvn ではなく classworlds の Launcher を直接叩くため(MavenHarness)、
+// zip 展開で失われる実行権限に依存しない
+val unpackMaven =
+ tasks.register("unpackMaven") {
+ from(zipTree(provider { mavenDistributionFiles.get().singleFile }))
+ into(mavenDistRoot)
+ }
+
+// フィクスチャのプレースホルダ置換(MavenHarness)へ渡す版。自版は親ビルドの gradle.properties、
+// Kotlin 版は共有カタログを正とする(docs/test/フィクスチャ構成.md §6)
+val enumizerOwnVersion: String =
+ Properties()
+ .apply { rootDir.resolve("../gradle.properties").inputStream().use(::load) }
+ .getProperty("enumizerVersion") ?: error("../gradle.properties に enumizerVersion が無い")
+
+val enumizerKotlinVersion: String = libs.versions.kotlin.get()
+
+// Maven のローカルリポジトリはテスト専用の場所へ隔離する(docs/test/フィクスチャ構成.md §6)。
+// 既定の ~/.m2/repository を使うと、Maven が取得する Gradle module metadata を伴わない成果物が
+// そこへ入り、mavenLocal() を宣言する TestKit フィクスチャの variant 解決を壊す
+val mavenLocalRepo = layout.buildDirectory.dir("maven-local-repo")
+
+// 本プラグインの成果物は publishToMavenLocal の出力(= ローカル Maven)を正とし、
+// 隔離リポジトリへは MavenHarness が複製する
+val publishedRepo = providers.systemProperty("user.home").map { "$it/.m2/repository" }
+
+// フィクスチャの展開先。失敗解析のため実行後は残し、次回の実行開始時に作り直す
+// (docs/test/フィクスチャ構成.md §6(展開先は実行前に回収する))
+val fixtureWorkRoot = layout.buildDirectory.dir("maven-fixtures")
+
+val cleanFixtureWorkRoot =
+ tasks.register("cleanFixtureWorkRoot") {
+ val workRoot = fixtureWorkRoot
+ doLast { workRoot.get().asFile.deleteRecursively() }
+ }
+
+tasks.test {
+ dependsOn(cleanFixtureWorkRoot, unpackMaven)
+ // フィクスチャはプラグイン一式をローカル Maven から解決するため、テスト前に公開しておく
+ // (docs/test/フィクスチャ構成.md §6 の local-repo 経路)
+ dependsOn(gradle.includedBuild("sealed-class-enumizer").task(":publishAllToMavenLocal"))
+ useJUnitPlatform()
+ systemProperty(
+ "enumizer.mavenHome",
+ mavenDistRoot.get().asFile.resolve("apache-maven-$mavenVersion").absolutePath,
+ )
+ systemProperty("enumizer.mavenLocalRepo", mavenLocalRepo.get().asFile.absolutePath)
+ systemProperty("enumizer.publishedRepo", publishedRepo.get())
+ systemProperty("enumizer.fixtureWorkRoot", fixtureWorkRoot.get().asFile.absolutePath)
+ systemProperty("enumizer.version", "$enumizerKotlinVersion-$enumizerOwnVersion")
+ systemProperty("enumizer.kotlinVersion", enumizerKotlinVersion)
+ // Maven のローカルリポジトリは共有資源であり、並行ダウンロードでの破損を避けるため直列に回す
+ // (フィクスチャは 1 プロジェクトずつで、TestKit のような多重化の利得も無い)
+ maxParallelForks = 1
+ testLogging {
+ events("passed", "skipped", "failed")
+ showStandardStreams = false
+ }
+}
diff --git a/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenBuildTest.kt b/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenBuildTest.kt
new file mode 100644
index 0000000..4c63576
--- /dev/null
+++ b/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenBuildTest.kt
@@ -0,0 +1,124 @@
+package io.github.projectmapk.maven
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+// 実 Maven での適用の観測(docs/test/ケース06-ビルド動態.md BLD-49〜BLD-53)。
+// label は設定で変わる値であり、フィクスチャの JUnit が出す OUT 行で照合する
+class MavenBuildTest {
+ private data class Applied(
+ val succeeded: Boolean,
+ // 「Applied plugin」は 1 コンパイレーションにつき 1 行。production / test の両方に載ることの観測
+ val appliedPluginCount: Int,
+ val observations: List,
+ )
+
+ private data class Failed(val succeeded: Boolean, val messageFound: Boolean)
+
+ @Test
+ fun appliesToProductionAndTestCompilations() {
+ val result = MavenHarness.run(MavenHarness.prepareFixture("basic"), "test")
+ assertEquals(
+ Applied(
+ succeeded = true,
+ appliedPluginCount = 2,
+ observations =
+ listOf(
+ // プロジェクト既定は未指定 = 組み込み既定(AS_DECLARED)
+ "OUT: si=Bar,FooBar",
+ "OUT: pinned=foo-bar",
+ "OUT: testOnly=OnlyLeaf",
+ ),
+ ),
+ observed(result),
+ result.output,
+ )
+ }
+
+ @Test
+ fun projectDefaultLabelCaseReachesTheCompiler() {
+ val result =
+ MavenHarness.run(MavenHarness.prepareFixture("basic"), "test", "-Pproject-default")
+ assertEquals(
+ Applied(
+ succeeded = true,
+ appliedPluginCount = 2,
+ observations =
+ listOf(
+ "OUT: si=BAR,FOO_BAR",
+ // 階層個別の具体指定はプロジェクト既定に勝つ
+ "OUT: pinned=foo-bar",
+ "OUT: testOnly=ONLY_LEAF",
+ ),
+ ),
+ observed(result),
+ result.output,
+ )
+ }
+
+ @Test
+ fun executionLevelPluginOptionOverridesTheProjectDefault() {
+ val result =
+ MavenHarness.run(
+ MavenHarness.prepareFixture("basic"),
+ "test",
+ "-Pproject-default,plugin-option",
+ )
+ assertEquals(
+ Applied(
+ succeeded = true,
+ appliedPluginCount = 2,
+ observations =
+ listOf("OUT: si=bar,foo_bar", "OUT: pinned=foo-bar", "OUT: testOnly=only_leaf"),
+ ),
+ observed(result),
+ result.output,
+ )
+ }
+
+ @Test
+ fun compilerDiagnosticFailsTheBuild() {
+ val result = MavenHarness.run(MavenHarness.prepareFixture("diag"), "compile")
+ assertEquals(
+ Failed(succeeded = false, messageFound = true),
+ Failed(
+ result.succeeded,
+ result.output.contains(
+ "'@Enumize' is applicable only to a sealed class or a sealed interface."
+ ),
+ ),
+ result.output,
+ )
+ }
+
+ @Test
+ fun unknownLabelCasePropertyFailsTheBuild() {
+ val result =
+ MavenHarness.run(MavenHarness.prepareFixture("diag"), "compile", "-Pbad-label-case")
+ assertEquals(
+ Failed(succeeded = false, messageFound = true),
+ Failed(
+ result.succeeded,
+ result.output.contains(
+ "Unknown value for the 'sealed-class-enumizer.labelCase' property: " +
+ "'Upper_Snake'. Expected one of: AS_DECLARED, UPPER_SNAKE_CASE, " +
+ "SNAKE_CASE, KEBAB_CASE"
+ ),
+ ),
+ result.output,
+ )
+ }
+
+ private fun observed(result: MavenResult): Applied =
+ Applied(
+ succeeded = result.succeeded,
+ appliedPluginCount = result.appliedPluginCount(PLUGIN_NAME),
+ observations = result.observations(),
+ )
+
+ private companion object {
+ // 利用側が へ書く名前(maven-plugin の
+ // SealedClassEnumizerMavenPluginExtension.PLUGIN_NAME)
+ const val PLUGIN_NAME: String = "sealed-class-enumizer"
+ }
+}
diff --git a/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenHarness.kt b/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenHarness.kt
new file mode 100644
index 0000000..9a8c2f1
--- /dev/null
+++ b/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenHarness.kt
@@ -0,0 +1,148 @@
+package io.github.projectmapk.maven
+
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.StandardCopyOption
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.io.path.copyTo
+import kotlin.io.path.createDirectories
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.readText
+import kotlin.io.path.relativeTo
+import kotlin.io.path.writeText
+
+// Maven フィクスチャの展開と Maven の起動を集約するハーネス(docs/test/フィクスチャ構成.md §6)。
+// フィクスチャは src/test/resources/fixtures/ に置き、テキストのプレースホルダを展開時に置換する:
+// - %%ENUMIZER_VERSION%% … 本プラグインのフル版(-<自版>)
+// - %%KOTLIN_VERSION%% … フィクスチャが適用する kotlin-maven-plugin の版
+// 版の値は親ビルドを正とし、maven-integration の test タスクが systemProperty で渡す。
+// プラグイン一式は publishToMavenLocal の出力を隔離リポジトリへ複製して解決する
+object MavenHarness {
+ private val mavenHome: Path = Path.of(requiredSystemProperty("enumizer.mavenHome"))
+
+ private val mavenLocalRepo: Path = Path.of(requiredSystemProperty("enumizer.mavenLocalRepo"))
+
+ private val publishedRepo: Path = Path.of(requiredSystemProperty("enumizer.publishedRepo"))
+
+ private val fixtureWorkRoot: Path = Path.of(requiredSystemProperty("enumizer.fixtureWorkRoot"))
+
+ private val enumizerVersion: String = requiredSystemProperty("enumizer.version")
+
+ private val kotlinVersion: String = requiredSystemProperty("enumizer.kotlinVersion")
+
+ private val fixturesRoot: Path =
+ Path.of(
+ requireNotNull(javaClass.classLoader.getResource("fixtures")) {
+ "src/test/resources/fixtures が見つからない"
+ }
+ .toURI()
+ )
+
+ private val workDirCounter = AtomicInteger()
+
+ private fun requiredSystemProperty(key: String): String =
+ requireNotNull(System.getProperty(key)) {
+ "システムプロパティ $key が未設定(maven-integration の test タスクが設定する)"
+ }
+
+ // フィクスチャ一式を一意な作業ディレクトリへ展開する(1 テスト 1 ディレクトリ)
+ fun prepareFixture(name: String): Path {
+ val source = fixturesRoot.resolve(name)
+ require(source.isDirectory()) { "フィクスチャ $name が存在しない: $source" }
+ val projectDir =
+ fixtureWorkRoot.resolve("$name-${workDirCounter.getAndIncrement()}").createDirectories()
+ Files.walk(source).use { paths ->
+ paths.forEach { path ->
+ val target = projectDir.resolve(path.relativeTo(source).toString())
+ when {
+ path.isDirectory() -> target.createDirectories()
+ isTextFile(path) -> {
+ target.parent.createDirectories()
+ target.writeText(expandTextFixture(path.readText()))
+ }
+ else -> {
+ target.parent.createDirectories()
+ path.copyTo(target, StandardCopyOption.REPLACE_EXISTING)
+ }
+ }
+ }
+ }
+ return projectDir
+ }
+
+ private fun expandTextFixture(text: String): String =
+ text
+ .replace("\r\n", "\n")
+ .replace("%%ENUMIZER_VERSION%%", enumizerVersion)
+ .replace("%%KOTLIN_VERSION%%", kotlinVersion)
+
+ private fun isTextFile(path: Path): Boolean {
+ val name = path.fileName.toString()
+ return name.endsWith(".xml") || name.endsWith(".kt") || name.endsWith(".properties")
+ }
+
+ // 起動は bin/mvn ではなく classworlds の Launcher を直接叩く(配布物の zip 展開では実行権限が
+ // 落ちるため、シェルスクリプト経由にするとホストによって起動できない)。
+ // JVM はテストと同じもの(toolchain)を使う
+ fun run(projectDir: Path, vararg arguments: String): MavenResult {
+ seedLocalRepository()
+ val command =
+ listOf(
+ javaExecutable().toString(),
+ "-classpath",
+ classWorldsJar().toString(),
+ "-Dclassworlds.conf=${mavenHome.resolve("bin").resolve("m2.conf")}",
+ "-Dmaven.home=$mavenHome",
+ "-Dmaven.multiModuleProjectDirectory=$projectDir",
+ "org.codehaus.plexus.classworlds.launcher.Launcher",
+ // 対話なし・ダウンロード進捗なしで、出力を照合可能な形に保つ
+ "-B",
+ "-ntp",
+ // ローカルリポジトリはテスト専用の場所へ隔離する(docs/test/フィクスチャ構成.md §6)。
+ // 既定の ~/.m2/repository を使うと、Maven が取得する Gradle module metadata を伴わない
+ // 成果物がそこへ入り、mavenLocal() を宣言する TestKit フィクスチャの variant 解決を壊す
+ "-Dmaven.repo.local=$mavenLocalRepo",
+ ) + arguments
+ val process =
+ ProcessBuilder(command).directory(projectDir.toFile()).redirectErrorStream(true).start()
+ // 出力を読み切ってから終了を待つ(先に待つとパイプが埋まって停止する)
+ val output = process.inputStream.bufferedReader().use { it.readText() }
+ return MavenResult(process.waitFor(), output)
+ }
+
+ // 隔離リポジトリへ本プラグインの成果物(publishToMavenLocal の出力)だけを複製する。
+ // 第三者依存は Maven が隔離先へ取得するため、~/.m2/repository は読むだけで書き換えない。
+ // `_remote.repositories` は複製しない(複製先では取得元が一致せず、再解決を要求されるため)
+ private fun seedLocalRepository() {
+ val source = publishedRepo.resolve(PUBLISHED_GROUP_PATH)
+ require(source.isDirectory()) { "publishToMavenLocal の出力が無い: $source(test タスクが前段で公開する)" }
+ val target = mavenLocalRepo.resolve(PUBLISHED_GROUP_PATH)
+ Files.walk(source).use { paths ->
+ paths.forEach { path ->
+ val destination = target.resolve(path.relativeTo(source).toString())
+ when {
+ path.isDirectory() -> destination.createDirectories()
+ path.fileName.toString() == "_remote.repositories" -> Unit
+ else -> {
+ destination.parent.createDirectories()
+ path.copyTo(destination, StandardCopyOption.REPLACE_EXISTING)
+ }
+ }
+ }
+ }
+ }
+
+ private fun javaExecutable(): Path {
+ val bin = Path.of(System.getProperty("java.home")).resolve("bin")
+ val windows = System.getProperty("os.name").startsWith("Windows", ignoreCase = true)
+ return bin.resolve(if (windows) "java.exe" else "java")
+ }
+
+ private fun classWorldsJar(): Path =
+ mavenHome.resolve("boot").listDirectoryEntries("plexus-classworlds-*.jar").singleOrNull()
+ ?: error("Maven 配布物に plexus-classworlds が 1 つ見つからない: $mavenHome")
+
+ // 複製対象は本プロジェクトの group 配下のみ(フィクスチャの依存指定と同じ座標)
+ private const val PUBLISHED_GROUP_PATH: String = "io/github/projectmapk"
+}
diff --git a/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenResult.kt b/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenResult.kt
new file mode 100644
index 0000000..c2966c9
--- /dev/null
+++ b/integration-test/maven-integration/src/test/kotlin/io/github/projectmapk/maven/MavenResult.kt
@@ -0,0 +1,19 @@
+package io.github.projectmapk.maven
+
+// Maven ビルド 1 本の結果。出力は標準出力・標準エラーを合流させたもの
+data class MavenResult(val exitCode: Int, val output: String) {
+ val succeeded: Boolean
+ get() = exitCode == 0
+
+ // フィクスチャの JUnit が出す観測行(surefire がテストの標準出力をそのまま流す)
+ fun observations(): List =
+ output.lines().map(String::trim).filter { it.startsWith(OBSERVATION_PREFIX) }
+
+ // コンパイレーション毎に 1 回出る mojo のログ。適用範囲(production / test)の観測に使う
+ fun appliedPluginCount(pluginName: String): Int =
+ output.lines().count { it.contains("Applied plugin: '$pluginName'") }
+
+ private companion object {
+ const val OBSERVATION_PREFIX: String = "OUT: "
+ }
+}
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/basic/pom.xml b/integration-test/maven-integration/src/test/resources/fixtures/basic/pom.xml
new file mode 100644
index 0000000..cbe2c8f
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/basic/pom.xml
@@ -0,0 +1,125 @@
+
+
+
+ 4.0.0
+
+ io.github.projectmapk.fixtures
+ maven-basic
+ 1.0.0
+
+
+ UTF-8
+ %%KOTLIN_VERSION%%
+ %%ENUMIZER_VERSION%%
+ 17
+
+
+
+
+
+
+ org.junit
+ junit-bom
+ 5.10.1
+ pom
+ import
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+
+ io.github.projectmapk
+ sealed-class-enumizer-runtime-api-jvm
+ ${enumizer.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+ compile
+
+
+ test-compile
+ test-compile
+ test-compile
+
+
+
+ ${maven.compiler.release}
+
+ sealed-class-enumizer
+
+
+
+
+
+ io.github.projectmapk
+ sealed-class-enumizer-maven-plugin
+ ${enumizer.version}
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
+
+
+
+ project-default
+
+ UPPER_SNAKE_CASE
+
+
+
+
+ plugin-option
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+
+
+
+
+
+
+
+
+
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/basic/src/main/kotlin/io/github/projectmapk/mavenfixture/Pinned.kt b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/main/kotlin/io/github/projectmapk/mavenfixture/Pinned.kt
new file mode 100644
index 0000000..5d4b4fd
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/main/kotlin/io/github/projectmapk/mavenfixture/Pinned.kt
@@ -0,0 +1,10 @@
+package io.github.projectmapk.mavenfixture
+
+import io.github.projectmapk.sealedClassEnumizer.Enumize
+import io.github.projectmapk.sealedClassEnumizer.LabelCase
+
+// 具体指定はプロジェクト既定にも実行単位の指定にも勝つ
+@Enumize(labelCase = LabelCase.KEBAB_CASE)
+sealed interface Pinned {
+ data object FooBar : Pinned
+}
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/basic/src/main/kotlin/io/github/projectmapk/mavenfixture/Si.kt b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/main/kotlin/io/github/projectmapk/mavenfixture/Si.kt
new file mode 100644
index 0000000..b3e71a1
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/main/kotlin/io/github/projectmapk/mavenfixture/Si.kt
@@ -0,0 +1,11 @@
+package io.github.projectmapk.mavenfixture
+
+import io.github.projectmapk.sealedClassEnumizer.Enumize
+
+// labelCase 未指定(= PROJECT_DEFAULT)の階層。プロジェクト既定の指定がここへ届く
+@Enumize
+sealed interface Si {
+ data object Bar : Si
+
+ data class FooBar(val v: Int) : Si
+}
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/basic/src/test/kotlin/io/github/projectmapk/mavenfixture/MavenFixtureTest.kt b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/test/kotlin/io/github/projectmapk/mavenfixture/MavenFixtureTest.kt
new file mode 100644
index 0000000..f8f65eb
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/test/kotlin/io/github/projectmapk/mavenfixture/MavenFixtureTest.kt
@@ -0,0 +1,16 @@
+package io.github.projectmapk.mavenfixture
+
+import org.junit.jupiter.api.Assertions.assertSame
+import org.junit.jupiter.api.Test
+
+// 生成 API の実行時観測。label は設定で変わるため OUT 行として出し(照合は maven-integration 側)、
+// 設定に依らない不変(valueOf が entries と同じ kind を返すこと)はここで表明する
+class MavenFixtureTest {
+ @Test
+ fun observeGeneratedApi() {
+ println("OUT: si=" + Si.Enumish.entries.joinToString(",") { it.label })
+ println("OUT: pinned=" + Pinned.Enumish.entries.joinToString(",") { it.label })
+ println("OUT: testOnly=" + TestOnly.Enumish.entries.joinToString(",") { it.label })
+ Si.Enumish.entries.forEach { assertSame(it, Si.Enumish.valueOf(it.label)) }
+ }
+}
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/basic/src/test/kotlin/io/github/projectmapk/mavenfixture/TestOnly.kt b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/test/kotlin/io/github/projectmapk/mavenfixture/TestOnly.kt
new file mode 100644
index 0000000..1da5cae
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/basic/src/test/kotlin/io/github/projectmapk/mavenfixture/TestOnly.kt
@@ -0,0 +1,9 @@
+package io.github.projectmapk.mavenfixture
+
+import io.github.projectmapk.sealedClassEnumizer.Enumize
+
+// test compilation にもプラグインが適用されることの観測用(生成 API を test 側の階層で使う)
+@Enumize
+sealed interface TestOnly {
+ data object OnlyLeaf : TestOnly
+}
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/diag/pom.xml b/integration-test/maven-integration/src/test/resources/fixtures/diag/pom.xml
new file mode 100644
index 0000000..2819baf
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/diag/pom.xml
@@ -0,0 +1,76 @@
+
+
+
+ 4.0.0
+
+ io.github.projectmapk.fixtures
+ maven-diag
+ 1.0.0
+
+
+ UTF-8
+ %%KOTLIN_VERSION%%
+ %%ENUMIZER_VERSION%%
+ 17
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ io.github.projectmapk
+ sealed-class-enumizer-runtime-api-jvm
+ ${enumizer.version}
+
+
+
+
+ src/main/kotlin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+ compile
+
+
+
+ ${maven.compiler.release}
+
+ sealed-class-enumizer
+
+
+
+
+ io.github.projectmapk
+ sealed-class-enumizer-maven-plugin
+ ${enumizer.version}
+
+
+
+
+
+
+
+
+
+ bad-label-case
+
+ Upper_Snake
+
+
+
+
diff --git a/integration-test/maven-integration/src/test/resources/fixtures/diag/src/main/kotlin/io/github/projectmapk/mavenfixture/NotSealed.kt b/integration-test/maven-integration/src/test/resources/fixtures/diag/src/main/kotlin/io/github/projectmapk/mavenfixture/NotSealed.kt
new file mode 100644
index 0000000..d951f34
--- /dev/null
+++ b/integration-test/maven-integration/src/test/resources/fixtures/diag/src/main/kotlin/io/github/projectmapk/mavenfixture/NotSealed.kt
@@ -0,0 +1,6 @@
+package io.github.projectmapk.mavenfixture
+
+import io.github.projectmapk.sealedClassEnumizer.Enumize
+
+// ENUMIZE_NOT_SEALED の発火(docs/概要.md §8)
+@Enumize class NotSealed
diff --git a/integration-test/settings.gradle.kts b/integration-test/settings.gradle.kts
index 9853f1e..b68325f 100644
--- a/integration-test/settings.gradle.kts
+++ b/integration-test/settings.gradle.kts
@@ -42,7 +42,7 @@ rootProject.name = "enumize-integration-test"
includeBuild("..")
-// docs/test/テスト戦略.md のモジュール一覧(8 モジュール)
+// docs/test/テスト戦略.md のモジュール一覧(9 モジュール)
include(
":producer-jvm",
":consumer-pure-jvm",
@@ -52,4 +52,5 @@ include(
":mpp-producer",
":mpp-consumer",
":gradle-integration",
+ ":maven-integration",
)
diff --git a/maven-plugin/build.gradle.kts b/maven-plugin/build.gradle.kts
new file mode 100644
index 0000000..b6b74c4
--- /dev/null
+++ b/maven-plugin/build.gradle.kts
@@ -0,0 +1,76 @@
+import com.vanniktech.maven.publish.JavadocJar
+import com.vanniktech.maven.publish.KotlinJvm
+
+plugins {
+ alias(libs.plugins.kotlin.jvm)
+ alias(libs.plugins.vanniktech.maven.publish)
+}
+
+kotlin { jvmToolchain(17) }
+
+dependencies {
+ // 拡張点(KotlinMavenPluginExtension / PluginOption)と Maven のモデル型は実行時に
+ // kotlin-maven-plugin の realm 側が供給するため、コンパイル時にのみ参照する
+ compileOnly(libs.kotlin.maven.plugin)
+ compileOnly(libs.maven.core)
+ // コンパイラプラグイン本体は POM の依存として配る。kotlin-maven-plugin は自身の を
+ // 推移解決してコンパイラのプラグインクラスパスへ載せるため、利用側は本成果物 1 つの宣言で足りる
+ api(project(":sealed-class-enumizer-compiler-plugin"))
+ testImplementation(libs.kotlin.maven.plugin)
+ testImplementation(libs.maven.core)
+ // 受け付ける LabelCase と runtime-api の宣言の一致検証(LabelCaseTest)のため、依存はテストにのみ持つ
+ testImplementation(project(":sealed-class-enumizer-runtime-api"))
+ testImplementation(libs.kotlin.test)
+}
+
+tasks.test { useJUnitPlatform() }
+
+// Maven Central 公開設定(POM の共通値はルート、モジュール別値は本モジュールの gradle.properties)。
+// 本成果物は Mojo ではなく kotlin-maven-plugin の拡張であり、plugin descriptor は持たない
+mavenPublishing {
+ publishToMavenCentral()
+ signAllPublications()
+ configure(KotlinJvm(javadocJar = JavadocJar.Empty()))
+}
+
+// SealedClassEnumizerCoordinates はビルドの値(version / Kotlin 版 / コンパイラプラグイン ID)から生成し、
+// ソース側での手動同期を持たない(gradle-plugin と同じ方式)
+val coordinatesDir = layout.buildDirectory.dir("generated/sources/coordinates/kotlin")
+
+val generateCoordinates =
+ tasks.register("generateCoordinates") {
+ val version = project.version.toString()
+ val kotlinVersion = libs.versions.kotlin.get()
+ val compilerPluginId = providers.gradleProperty("compilerPluginId").get()
+ val outputDir = coordinatesDir
+ inputs.property("version", version)
+ inputs.property("kotlinVersion", kotlinVersion)
+ inputs.property("compilerPluginId", compilerPluginId)
+ outputs.dir(outputDir)
+ doLast {
+ val file =
+ outputDir
+ .get()
+ .file(
+ "io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerCoordinates.kt"
+ )
+ .asFile
+ file.parentFile.mkdirs()
+ file.writeText(
+ """
+ |// generateCoordinates タスクが生成するファイル(手動編集しない)。
+ |// 値の正はビルド側(version / Kotlin 版 / コンパイラプラグイン ID)にある
+ |package io.github.projectmapk.sealedClassEnumizer.maven
+ |
+ |internal object SealedClassEnumizerCoordinates {
+ | const val VERSION: String = "$version"
+ | const val KOTLIN_VERSION: String = "$kotlinVersion"
+ | const val COMPILER_PLUGIN_ID: String = "$compilerPluginId"
+ |}
+ |"""
+ .trimMargin()
+ )
+ }
+ }
+
+kotlin.sourceSets.getByName("main").kotlin.srcDir(generateCoordinates)
diff --git a/maven-plugin/gradle.properties b/maven-plugin/gradle.properties
new file mode 100644
index 0000000..6d7d1c4
--- /dev/null
+++ b/maven-plugin/gradle.properties
@@ -0,0 +1,3 @@
+# POM のモジュール別値(共通値はルートの gradle.properties)
+POM_NAME=Sealed Class Enumizer Maven Plugin
+POM_DESCRIPTION=Maven plugin that applies the sealed-class-enumizer compiler plugin to kotlin-maven-plugin compilations.
diff --git a/maven-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/LabelCase.kt b/maven-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/LabelCase.kt
new file mode 100644
index 0000000..f92d0e7
--- /dev/null
+++ b/maven-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/LabelCase.kt
@@ -0,0 +1,27 @@
+package io.github.projectmapk.sealedClassEnumizer.maven
+
+// runtime-api の LabelCase から PROJECT_DEFAULT を除いた具体ケースと 1:1 対応する。
+// エントリ名の一致は LabelCaseTest がガードし、値はコンパイラプラグインの labelCase オプションへ
+// エントリ名のまま渡される
+/**
+ * Label cases accepted as the project-wide default, that is, as the value of the
+ * `sealed-class-enumizer.labelCase` property described on
+ * [SealedClassEnumizerMavenPluginExtension].
+ *
+ * Mirrors the runtime API's `LabelCase` without `PROJECT_DEFAULT` — the project default is what
+ * this property defines, so a "read the project default" entry would be self-referential here. The
+ * conversion rules are documented on the runtime API's `LabelCase` and are frozen across releases.
+ */
+enum class LabelCase {
+ /** No conversion: the label is the declared simple name. */
+ AS_DECLARED,
+
+ /** `FooBar` → `FOO_BAR`. */
+ UPPER_SNAKE_CASE,
+
+ /** `FooBar` → `foo_bar`. */
+ SNAKE_CASE,
+
+ /** `FooBar` → `foo-bar`. */
+ KEBAB_CASE,
+}
diff --git a/maven-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerMavenPluginExtension.kt b/maven-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerMavenPluginExtension.kt
new file mode 100644
index 0000000..704c4b5
--- /dev/null
+++ b/maven-plugin/src/main/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerMavenPluginExtension.kt
@@ -0,0 +1,163 @@
+package io.github.projectmapk.sealedClassEnumizer.maven
+
+import org.apache.maven.plugin.MojoExecution
+import org.apache.maven.project.MavenProject
+import org.codehaus.plexus.logging.Logger
+import org.jetbrains.kotlin.maven.KotlinMavenPluginExtension
+import org.jetbrains.kotlin.maven.PluginOption
+
+/**
+ * Applies the sealed-class-enumizer compiler plugin to the kotlin-maven-plugin compilations that
+ * list it, and passes the project-wide default label case to it.
+ *
+ * Declare the artifact as a dependency of kotlin-maven-plugin and name the plugin in
+ * `compilerPlugins`; the compiler plugin itself comes along as a transitive dependency:
+ * ```xml
+ *
+ * org.jetbrains.kotlin
+ * kotlin-maven-plugin
+ *
+ *
+ * sealed-class-enumizer
+ *
+ *
+ *
+ *
+ * io.github.projectmapk
+ * sealed-class-enumizer-maven-plugin
+ * ${sealed-class-enumizer.version}
+ *
+ *
+ *
+ * ```
+ *
+ * The configuration above applies to every execution of kotlin-maven-plugin, production and test
+ * compilations alike. The runtime API is not added automatically: declare
+ * `io.github.projectmapk:sealed-class-enumizer-runtime-api-jvm` in the project `dependencies`,
+ * since the generated API exposes its types as supertypes.
+ *
+ * The project-wide default label case is read from the `sealed-class-enumizer.labelCase` property
+ * and defaults to [LabelCase.AS_DECLARED] (no conversion). It applies to hierarchies whose
+ * `@Enumize` does not specify a concrete `labelCase`:
+ * ```xml
+ *
+ * UPPER_SNAKE_CASE
+ *
+ * ```
+ *
+ * The same option can also be given through the kotlin-maven-plugin `pluginOptions`
+ * (`sealed-class-enumizer:labelCase=UPPER_SNAKE_CASE`). Such a declaration takes precedence: the
+ * property is then not passed at all, since the compiler option accepts a single value.
+ */
+class SealedClassEnumizerMavenPluginExtension : KotlinMavenPluginExtension {
+ // Plexus の requirement 注入(META-INF/plexus/components.xml)で設定される。
+ // 用途は対応外 Kotlin の警告のみで、オプションの組み立てには関与しない
+ lateinit var logger: Logger
+
+ override fun getCompilerPluginId(): String = SealedClassEnumizerCoordinates.COMPILER_PLUGIN_ID
+
+ // goal による絞り込みは行わない(compile / test-compile の双方へ適用する)。
+ // 適用範囲は利用側が の記載位置で選ぶ
+ override fun isApplicable(project: MavenProject, execution: MojoExecution): Boolean = true
+
+ override fun getPluginOptions(
+ project: MavenProject,
+ execution: MojoExecution,
+ ): List {
+ warnIfUnsupportedKotlin(project, execution)
+ // 実行単位の直接指定がある場合はプロジェクト既定を渡さない。オプションは複数指定を
+ // 受け付けず(コンパイラ側が「Multiple values are not allowed」で失敗する)、
+ // 直接指定は mojo 自身が渡すため、こちらが降りることで優先関係が成立する
+ if (declaresLabelCaseOption(execution)) {
+ return emptyList()
+ }
+ // プロジェクト既定の label ケースを常に具体値で渡す(docs/概要.md §4)。
+ // キーはコンパイラプラグインの EnumizeCommandLineProcessor.LABEL_CASE_OPTION_NAME と
+ // 一致していなければならない
+ return listOf(
+ PluginOption(
+ PLUGIN_NAME,
+ getCompilerPluginId(),
+ LABEL_CASE_OPTION_NAME,
+ resolveLabelCase(project).name,
+ )
+ )
+ }
+
+ // kotlin-maven-plugin の に本プラグインの labelCase 指定が在るか
+ // (表記は mojo の解釈と同じ <プラグイン名>:<キー>=<値>)
+ private fun declaresLabelCaseOption(execution: MojoExecution): Boolean {
+ val declared = execution.configuration?.getChild(PLUGIN_OPTIONS_ELEMENT) ?: return false
+ return declared.children.any {
+ it.value?.trim()?.startsWith("$PLUGIN_NAME:$LABEL_CASE_OPTION_NAME=") == true
+ }
+ }
+
+ // 対応外の Kotlin への適用は警告のみとする(サポートは KOTLIN_VERSION と同一マイナー。
+ // 非互換の実害はコンパイル時に顕在化するため、原因を示す警告に留めてエラーにはしない)
+ private fun warnIfUnsupportedKotlin(project: MavenProject, execution: MojoExecution) {
+ // 適用先の Kotlin 版は、本拡張を読み込んでいる kotlin-maven-plugin 自身の版である
+ val appliedKotlinVersion = execution.plugin?.version ?: return
+ if (
+ isSupportedKotlinVersion(
+ appliedKotlinVersion,
+ SealedClassEnumizerCoordinates.KOTLIN_VERSION,
+ )
+ ) {
+ return
+ }
+ logger.warn(unsupportedKotlinWarning(appliedKotlinVersion, project.artifactId))
+ }
+
+ companion object {
+ // 利用側が へ書く名前であり、Plexus の role-hint(components.xml)と
+ // 一致していなければならない(PlexusComponentsTest がガードする)
+ const val PLUGIN_NAME: String = "sealed-class-enumizer"
+
+ /**
+ * Name of the property holding the project-wide default label case. See
+ * [SealedClassEnumizerMavenPluginExtension].
+ */
+ const val LABEL_CASE_PROPERTY: String = "$PLUGIN_NAME.labelCase"
+
+ private const val LABEL_CASE_OPTION_NAME: String = "labelCase"
+
+ // kotlin-maven-plugin の設定要素名(利用者が直接指定を書く場所)
+ private const val PLUGIN_OPTIONS_ELEMENT: String = "pluginOptions"
+ }
+}
+
+// プロジェクト既定の label ケース。未指定時の既定はコンパイラプラグインの組み込み既定
+// (EnumizeLabelCase.BUILT_IN_DEFAULT)と同値の AS_DECLARED に固定し、二重既定の乖離を防ぐ。
+// 不正値は診断が読みにくいコンパイラ側の失敗になる前に、設定面の名前を示して弾く
+internal fun resolveLabelCase(project: MavenProject): LabelCase {
+ val declared =
+ project.properties
+ .getProperty(SealedClassEnumizerMavenPluginExtension.LABEL_CASE_PROPERTY)
+ ?.trim()
+ .orEmpty()
+ if (declared.isEmpty()) {
+ return LabelCase.AS_DECLARED
+ }
+ return LabelCase.entries.firstOrNull { it.name == declared }
+ ?: throw IllegalArgumentException(
+ "Unknown value for the " +
+ "'${SealedClassEnumizerMavenPluginExtension.LABEL_CASE_PROPERTY}' property: " +
+ "'$declared'. Expected one of: ${LabelCase.entries.joinToString { it.name }}"
+ )
+}
+
+internal fun unsupportedKotlinWarning(appliedKotlinVersion: String, projectName: String): String =
+ "sealed-class-enumizer ${SealedClassEnumizerCoordinates.VERSION} targets Kotlin " +
+ "${SealedClassEnumizerCoordinates.KOTLIN_VERSION} and is not verified against Kotlin " +
+ "$appliedKotlinVersion applied to $projectName. Compilation may fail; use the plugin " +
+ "version matching your Kotlin version."
+
+// サポート判定はマイナー一致とする(同一 . ならパッチ・プレリリースの差は対応内。
+// 版形式 -<自版> の下で、成果物は対応マイナーの全パッチへ同一物を配るため)
+internal fun isSupportedKotlinVersion(
+ appliedKotlinVersion: String,
+ supportedKotlinVersion: String,
+): Boolean = majorMinorOf(appliedKotlinVersion) == majorMinorOf(supportedKotlinVersion)
+
+private fun majorMinorOf(version: String): String = version.split('.').take(2).joinToString(".")
diff --git a/maven-plugin/src/main/resources/META-INF/plexus/components.xml b/maven-plugin/src/main/resources/META-INF/plexus/components.xml
new file mode 100644
index 0000000..8f48d21
--- /dev/null
+++ b/maven-plugin/src/main/resources/META-INF/plexus/components.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+ org.jetbrains.kotlin.maven.KotlinMavenPluginExtension
+ sealed-class-enumizer
+ io.github.projectmapk.sealedClassEnumizer.maven.SealedClassEnumizerMavenPluginExtension
+ false
+
+
+ org.codehaus.plexus.logging.Logger
+ logger
+
+
+
+
+
diff --git a/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/KotlinVersionSupportTest.kt b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/KotlinVersionSupportTest.kt
new file mode 100644
index 0000000..c08afe4
--- /dev/null
+++ b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/KotlinVersionSupportTest.kt
@@ -0,0 +1,28 @@
+package io.github.projectmapk.sealedClassEnumizer.maven
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+// Kotlin バージョンのサポート判定(マイナー一致)の期待値固定。
+// パッチ差・プレリリース表記・マイナー / メジャー差を覆う
+class KotlinVersionSupportTest {
+ private data class Case(val applied: String, val supported: String, val expected: Boolean)
+
+ @Test
+ fun minorLevelMatchDecidesSupport() {
+ val cases =
+ listOf(
+ Case(applied = "2.4.0", supported = "2.4.0", expected = true),
+ Case(applied = "2.4.10", supported = "2.4.0", expected = true),
+ Case(applied = "2.4.20-Beta1", supported = "2.4.0", expected = true),
+ Case(applied = "2.5.0", supported = "2.4.0", expected = false),
+ Case(applied = "2.5.0-Beta1", supported = "2.4.20", expected = false),
+ Case(applied = "2.3.0", supported = "2.4.0", expected = false),
+ Case(applied = "3.0.0", supported = "2.4.0", expected = false),
+ )
+ assertEquals(
+ cases.map { it.expected },
+ cases.map { isSupportedKotlinVersion(it.applied, it.supported) },
+ )
+ }
+}
diff --git a/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/LabelCaseTest.kt b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/LabelCaseTest.kt
new file mode 100644
index 0000000..dc3b176
--- /dev/null
+++ b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/LabelCaseTest.kt
@@ -0,0 +1,20 @@
+package io.github.projectmapk.sealedClassEnumizer.maven
+
+import io.github.projectmapk.sealedClassEnumizer.LabelCase as RuntimeLabelCase
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+// 受け付ける LabelCase は runtime-api の LabelCase から PROJECT_DEFAULT を除いた具体ケースと 1:1 対応する
+// (docs/概要.md §4)。宣言はモジュール毎に独立させているため、名前集合と並びの一致をここで担保する:
+// runtime-api 側のエントリを追加・改名すると、参照が追随するこのテストが失敗し、更新漏れを検知できる
+class LabelCaseTest {
+ @Test
+ fun `受け付ける LabelCase は runtime-api の具体ケースと一致する`() {
+ assertEquals(
+ RuntimeLabelCase.entries
+ .filterNot { it == RuntimeLabelCase.PROJECT_DEFAULT }
+ .map { it.name },
+ LabelCase.entries.map { it.name },
+ )
+ }
+}
diff --git a/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/PlexusComponentsTest.kt b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/PlexusComponentsTest.kt
new file mode 100644
index 0000000..e02db85
--- /dev/null
+++ b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/PlexusComponentsTest.kt
@@ -0,0 +1,87 @@
+package io.github.projectmapk.sealedClassEnumizer.maven
+
+import javax.xml.parsers.DocumentBuilderFactory
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import org.codehaus.plexus.logging.Logger
+import org.jetbrains.kotlin.maven.KotlinMavenPluginExtension
+import org.w3c.dom.Element
+
+// Plexus コンポーネント登録(META-INF/plexus/components.xml)と実装の対応を固定する。
+// 登録は実行時にしか効かず、名前がずれても本モジュールのコンパイルは通るため、
+// role / role-hint / implementation / requirement のフィールドをここで実体と突き合わせる
+class PlexusComponentsTest {
+ private data class Registration(
+ val role: String,
+ val roleHint: String,
+ val implementation: String,
+ val requirementRole: String,
+ val requirementField: String,
+ // requirement の注入先が実装に在ることの確認(型は宣言された role と一致していなければならない)
+ val injectedFieldType: String,
+ val implementsExtensionPoint: Boolean,
+ )
+
+ @Test
+ fun componentDescriptorMatchesImplementation() {
+ val component = readSingleComponent()
+ val requirement =
+ singleChildElement(component, "requirements").let {
+ singleChildElement(it, "requirement")
+ }
+ val implementationClass = Class.forName(text(component, "implementation"))
+ val injectedField = implementationClass.getDeclaredField(text(requirement, "field-name"))
+ assertEquals(
+ Registration(
+ role = KotlinMavenPluginExtension::class.java.name,
+ roleHint = SealedClassEnumizerMavenPluginExtension.PLUGIN_NAME,
+ implementation = SealedClassEnumizerMavenPluginExtension::class.java.name,
+ requirementRole = Logger::class.java.name,
+ requirementField = "logger",
+ injectedFieldType = Logger::class.java.name,
+ implementsExtensionPoint = true,
+ ),
+ Registration(
+ role = text(component, "role"),
+ roleHint = text(component, "role-hint"),
+ implementation = implementationClass.name,
+ requirementRole = text(requirement, "role"),
+ requirementField = injectedField.name,
+ injectedFieldType = injectedField.type.name,
+ implementsExtensionPoint =
+ KotlinMavenPluginExtension::class.java.isAssignableFrom(implementationClass),
+ ),
+ )
+ }
+
+ private fun readSingleComponent(): Element {
+ val resource =
+ requireNotNull(javaClass.classLoader.getResource(COMPONENTS_RESOURCE)) {
+ "$COMPONENTS_RESOURCE がクラスパスに無い"
+ }
+ val document =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(resource.toString())
+ return singleChildElement(document.documentElement, "components").let {
+ singleChildElement(it, "component")
+ }
+ }
+
+ private fun singleChildElement(parent: Element, name: String): Element {
+ val children = childElements(parent, name)
+ assertEquals(1, children.size, "$name の要素数")
+ return children.single()
+ }
+
+ private fun childElements(parent: Element, name: String): List =
+ (0 until parent.childNodes.length)
+ .map(parent.childNodes::item)
+ .filterIsInstance()
+ .filter { it.tagName == name }
+
+ private fun text(parent: Element, name: String): String =
+ singleChildElement(parent, name).textContent.trim()
+
+ private companion object {
+ const val COMPONENTS_RESOURCE: String = "META-INF/plexus/components.xml"
+ }
+}
diff --git a/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerMavenPluginExtensionTest.kt b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerMavenPluginExtensionTest.kt
new file mode 100644
index 0000000..c6b4aa8
--- /dev/null
+++ b/maven-plugin/src/test/kotlin/io/github/projectmapk/sealedClassEnumizer/maven/SealedClassEnumizerMavenPluginExtensionTest.kt
@@ -0,0 +1,196 @@
+package io.github.projectmapk.sealedClassEnumizer.maven
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.apache.maven.model.Model
+import org.apache.maven.model.Plugin
+import org.apache.maven.plugin.MojoExecution
+import org.apache.maven.project.MavenProject
+import org.codehaus.plexus.logging.AbstractLogger
+import org.codehaus.plexus.logging.Logger
+import org.codehaus.plexus.util.xml.Xpp3Dom
+
+// kotlin-maven-plugin の拡張点としての振る舞いの固定。
+// 観測はコンパイラへ渡る形(PluginOption.toString = plugin::<キー>=<値>)と、
+// mojo がオプションの振り分けに使う各フィールドで行う
+class SealedClassEnumizerMavenPluginExtensionTest {
+ private data class LabelCaseCase(val declared: String?, val expected: String)
+
+ private data class OptionFields(
+ val pluginName: String,
+ val pluginId: String,
+ val key: String,
+ val value: String,
+ )
+
+ // 警告の発火を観測するための Logger。Plexus は requirement で実装を注入するため、
+ // テストでは記録用の実装を差し込む
+ private class RecordingLogger : AbstractLogger(Logger.LEVEL_INFO, "test") {
+ val warnings: MutableList = mutableListOf()
+
+ override fun debug(message: String, throwable: Throwable?) = Unit
+
+ override fun info(message: String, throwable: Throwable?) = Unit
+
+ override fun warn(message: String, throwable: Throwable?) {
+ warnings += message
+ }
+
+ override fun error(message: String, throwable: Throwable?) = Unit
+
+ override fun fatalError(message: String, throwable: Throwable?) = Unit
+
+ override fun getChildLogger(name: String): Logger = this
+ }
+
+ @Test
+ fun labelCaseOptionCarriesTheProjectDefault() {
+ val cases =
+ listOf(
+ // 未指定はコンパイラプラグインの組み込み既定と同値(docs/概要.md §4)
+ LabelCaseCase(declared = null, expected = "AS_DECLARED"),
+ LabelCaseCase(declared = "UPPER_SNAKE_CASE", expected = "UPPER_SNAKE_CASE"),
+ LabelCaseCase(declared = " KEBAB_CASE ", expected = "KEBAB_CASE"),
+ LabelCaseCase(declared = " ", expected = "AS_DECLARED"),
+ )
+ assertEquals(
+ cases.map {
+ "plugin:io.github.projectmapk.sealed-class-enumizer:labelCase=${it.expected}"
+ },
+ cases.map {
+ extension().getPluginOptions(project(it.declared), execution()).single().toString()
+ },
+ )
+ }
+
+ @Test
+ fun pluginOptionIdentifiesTheCompilerPlugin() {
+ val option = extension().getPluginOptions(project(), execution()).single()
+ assertEquals(
+ OptionFields(
+ // mojo は の接頭辞(= の名前)でオプションを束ねる
+ pluginName = "sealed-class-enumizer",
+ pluginId = "io.github.projectmapk.sealed-class-enumizer",
+ key = "labelCase",
+ value = "AS_DECLARED",
+ ),
+ OptionFields(option.pluginName, option.pluginId, option.key, option.value),
+ )
+ }
+
+ @Test
+ fun executionLevelOptionSuppressesTheProjectDefault() {
+ // 直接指定は mojo 自身が渡す。オプションは複数指定を受け付けないため、こちらは何も渡さない
+ val declared =
+ listOf(
+ "sealed-class-enumizer:labelCase=SNAKE_CASE",
+ // 他プラグイン向け・他キーの指定は抑止の条件にならない
+ "all-open:annotation=example.Ann",
+ "sealed-class-enumizer:other=x",
+ )
+ assertEquals(
+ listOf(0, 1, 1),
+ declared.map {
+ extension()
+ .getPluginOptions(project("UPPER_SNAKE_CASE"), execution(pluginOptions = it))
+ .size
+ },
+ )
+ }
+
+ @Test
+ fun unknownLabelCaseIsRejectedWithTheAcceptedValues() {
+ val failure =
+ assertFailsWith {
+ extension().getPluginOptions(project("Upper_Snake"), execution())
+ }
+ assertEquals(
+ "Unknown value for the 'sealed-class-enumizer.labelCase' property: 'Upper_Snake'. " +
+ "Expected one of: AS_DECLARED, UPPER_SNAKE_CASE, SNAKE_CASE, KEBAB_CASE",
+ failure.message,
+ )
+ }
+
+ @Test
+ fun appliesToEveryCompilation() {
+ val goals = listOf("compile", "test-compile")
+ assertEquals(
+ goals.map { true },
+ goals.map { extension().isApplicable(project(), execution(goal = it)) },
+ )
+ }
+
+ @Test
+ fun onlyUnsupportedKotlinMinorIsWarned() {
+ val logger = RecordingLogger()
+ val extension = SealedClassEnumizerMavenPluginExtension().apply { this.logger = logger }
+ listOf("2.9.0", SealedClassEnumizerCoordinates.KOTLIN_VERSION).forEach {
+ extension.getPluginOptions(project(), execution(kotlinVersion = it))
+ }
+ assertEquals(
+ listOf(
+ "sealed-class-enumizer ${SealedClassEnumizerCoordinates.VERSION} targets Kotlin " +
+ "${SealedClassEnumizerCoordinates.KOTLIN_VERSION} and is not verified against " +
+ "Kotlin 2.9.0 applied to fixture. Compilation may fail; use the plugin " +
+ "version matching your Kotlin version."
+ ),
+ logger.warnings,
+ )
+ }
+
+ @Test
+ fun unknownKotlinVersionIsNotWarned() {
+ // 版が読めない構成では警告しない(Logger 未注入でもオプションの組み立ては成立する)
+ val options =
+ SealedClassEnumizerMavenPluginExtension()
+ .getPluginOptions(project(), execution(kotlinVersion = null))
+ assertEquals(1, options.size)
+ }
+
+ private fun extension(): SealedClassEnumizerMavenPluginExtension =
+ SealedClassEnumizerMavenPluginExtension().apply { logger = RecordingLogger() }
+
+ private fun project(labelCase: String? = null): MavenProject {
+ val model =
+ Model().apply {
+ groupId = "io.github.projectmapk.fixtures"
+ artifactId = "fixture"
+ version = "1.0.0"
+ }
+ labelCase?.let {
+ model.properties.setProperty(
+ SealedClassEnumizerMavenPluginExtension.LABEL_CASE_PROPERTY,
+ it,
+ )
+ }
+ return MavenProject(model)
+ }
+
+ // 適用先の Kotlin 版は kotlin-maven-plugin 自身の版として渡る。
+ // pluginOptions は mojo へ渡る実効設定(プラグイン単位・実行単位の合成後)として観測される
+ private fun execution(
+ goal: String = "compile",
+ kotlinVersion: String? = SealedClassEnumizerCoordinates.KOTLIN_VERSION,
+ pluginOptions: String? = null,
+ ): MojoExecution {
+ val plugin =
+ Plugin().apply {
+ groupId = "org.jetbrains.kotlin"
+ artifactId = "kotlin-maven-plugin"
+ version = kotlinVersion
+ }
+ val execution = MojoExecution(plugin, goal, "default-$goal")
+ pluginOptions?.let {
+ execution.configuration =
+ Xpp3Dom("configuration").apply {
+ addChild(
+ Xpp3Dom("pluginOptions").apply {
+ addChild(Xpp3Dom("option").apply { value = it })
+ }
+ )
+ }
+ }
+ return execution
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1f8f08e..3649082 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -17,7 +17,7 @@ dependencyResolutionManagement {
}
}
-include(":runtime-api", ":compiler-plugin", ":gradle-plugin")
+include(":runtime-api", ":compiler-plugin", ":gradle-plugin", ":maven-plugin")
// 公開 artifactId は「sealed-class-enumizer-」接頭辞付きとする(group 直下に汎用名を置かない)。
// artifactId の源泉であるプロジェクト名ごと揃えることで、KMP のターゲット別 publication の派生名と、
@@ -28,3 +28,5 @@ project(":runtime-api").name = "sealed-class-enumizer-runtime-api"
project(":compiler-plugin").name = "sealed-class-enumizer-compiler-plugin"
project(":gradle-plugin").name = "sealed-class-enumizer-gradle-plugin"
+
+project(":maven-plugin").name = "sealed-class-enumizer-maven-plugin"