1 Commits
Author SHA1 Message Date
Bitsy 6dbdc93122 style: replace em dashes with plain punctuation 2026-08-09 21:46:02 +02:00
21 changed files with 80 additions and 80 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
echo "Release $VERSION already exists — skipping." echo "Release $VERSION already exists - skipping."
else else
gh release create "$VERSION" \ gh release create "$VERSION" \
--repo "$GITHUB_REPOSITORY" \ --repo "$GITHUB_REPOSITORY" \
+1 -1
View File
@@ -5,6 +5,6 @@
local.properties local.properties
.DS_Store .DS_Store
# Local credential overrides — never commit tokens # Local credential overrides - never commit tokens
gradle.properties.local gradle.properties.local
secrets.properties secrets.properties
+27 -27
View File
@@ -11,7 +11,7 @@ This file provides guidance to AI agents (Claude, Copilot, Codex, etc.) working
# Run unit tests only # Run unit tests only
./gradlew test ./gradlew test
# Run functional tests (Gradle TestKit — starts real Gradle builds) # Run functional tests (Gradle TestKit - starts real Gradle builds)
./gradlew functionalTest ./gradlew functionalTest
# Run a single functional test # Run a single functional test
@@ -51,20 +51,20 @@ region) each with their own variants.
**Configuration cache compatibility** is a hard requirement throughout. This means: **Configuration cache compatibility** is a hard requirement throughout. This means:
- Never capture `project` inside a `Provider.map {}` or `Provider.zip {}` lambda, and **never - Never capture `project` inside a `Provider.map {}` or `Provider.zip {}` lambda, and **never
access `project` inside a `@TaskAction`** — both break caching. Use only declared access `project` inside a `@TaskAction`** - both break caching. Use only declared
`@Input`/`@OutputDirectory` properties inside task actions. `@Input`/`@OutputDirectory` properties inside task actions.
- Use `Class<T>` (`.javaObjectType`) instead of `KClass<T>` — Kotlin's `KClass` uses - Use `Class<T>` (`.javaObjectType`) instead of `KClass<T>` - Kotlin's `KClass` uses
`SoftReference` internally which Gradle can't serialize. `SoftReference` internally which Gradle can't serialize.
- Use `gradlePropertiesPrefixedBy()` to read groups of properties — **note:** in Gradle 9.x this - Use `gradlePropertiesPrefixedBy()` to read groups of properties - **note:** in Gradle 9.x this
returns full property names as keys (prefix is NOT stripped), so always check both returns full property names as keys (prefix is NOT stripped), so always check both
`dimProps["env"]` and `dimProps["konfig.dimension.env"]`. `dimProps["env"]` and `dimProps["konfig.dimension.env"]`.
- All DSL field values are wrapped in `Provider<T>` from the start — literals via - All DSL field values are wrapped in `Provider<T>` from the start - literals via
`constantProvider(value)` (a hand-written `ConstantProvider<T>`), external values via `constantProvider(value)` (a hand-written `ConstantProvider<T>`), external values via
`providers.gradleProperty()` / `providers.environmentVariable()` etc. **Never store `providers.gradleProperty()` / `providers.environmentVariable()` etc. **Never store
`ProviderFactory` anywhere in the DSL object graph** — it is not config-cache serializable. `ProviderFactory` anywhere in the DSL object graph** - it is not config-cache serializable.
- `forceRegen` (`konfig.force` property) is evaluated eagerly at configuration time as a plain - `forceRegen` (`konfig.force` property) is evaluated eagerly at configuration time as a plain
`Boolean` via `providers.gradleProperty("konfig.force").isPresent` — not inside a provider `Boolean` via `providers.gradleProperty("konfig.force").isPresent` - not inside a provider
lambda — so the value is captured by value and is config-cache safe. lambda - so the value is captured by value and is config-cache safe.
**Dimension data in task inputs** uses flat-map encoding (`"<dimName>|<fieldName>"` as map keys) **Dimension data in task inputs** uses flat-map encoding (`"<dimName>|<fieldName>"` as map keys)
in `MapProperty<String, String>` rather than a managed-type `ListProperty`. Values are in `MapProperty<String, String>` rather than a managed-type `ListProperty`. Values are
@@ -74,14 +74,14 @@ per-type maps down to one per scope and avoids Gradle's `@Nested` managed-type r
### DSL design ### DSL design
- **`@KonfigDsl` / `@DslMarker`** is applied to all DSL scope classes to prevent accidental scope leakage. - **`@KonfigDsl` / `@DslMarker`** is applied to all DSL scope classes to prevent accidental scope leakage.
- **`ConstantProvider<T>`** wraps literal values — no `ProviderFactory` anywhere in the DSL object graph. - **`ConstantProvider<T>`** wraps literal values - no `ProviderFactory` anywhere in the DSL object graph.
- **`field()` at the top level** returns `FieldHandle<T>` which exposes `.debug(value)` and - **`field()` at the top level** returns `FieldHandle<T>` which exposes `.debug(value)` and
`.release(value)`, both returning `Unit` — chaining beyond the first call is intentionally impossible. `.release(value)`, both returning `Unit` - chaining beyond the first call is intentionally impossible.
- **`debug {}` / `release {}` scope blocks** use `BuildTypedFieldDeclScope` as receiver — - **`debug {}` / `release {}` scope blocks** use `BuildTypedFieldDeclScope` as receiver -
`field()` inside these returns `Unit`, since the build type is already fixed by the enclosing scope. `field()` inside these returns `Unit`, since the build type is already fixed by the enclosing scope.
- **`common {}` block in `DimensionConfig`** — shared fallback fields for all variants; merged in - **`common {}` block in `DimensionConfig`** - shared fallback fields for all variants; merged in
plugin with variant fields taking precedence. plugin with variant fields taking precedence.
- **Plain `var` properties on `KonfigExtension`** — `objectPackage`, `objectName`, - **Plain `var` properties on `KonfigExtension`** - `objectPackage`, `objectName`,
`objectVisibility` are user-facing `var` properties backed by internal `Property<T>` `objectVisibility` are user-facing `var` properties backed by internal `Property<T>`
(`objectPackageProp`, `objectNameProp`, `objectVisibilityProp`) used for lazy task wiring. (`objectPackageProp`, `objectNameProp`, `objectVisibilityProp`) used for lazy task wiring.
@@ -91,7 +91,7 @@ per-type maps down to one per scope and avoids Gradle's `@Nested` managed-type r
2. `konfig.properties` file in the project directory: `konfig.dimension.<name>=<variant>` 2. `konfig.properties` file in the project directory: `konfig.dimension.<name>=<variant>`
3. Task-name detection (variant name substring match, if not disabled by `konfig.android.flavordetection=false`) 3. Task-name detection (variant name substring match, if not disabled by `konfig.android.flavordetection=false`)
4. `defaultTo` fallback declared in DSL 4. `defaultTo` fallback declared in DSL
5. **Omitted silently** if none of the above — no crash, dimension object not generated 5. **Omitted silently** if none of the above - no crash, dimension object not generated
`resolveWithSource()` in `KonfigPlugin` returns a tab-separated `"<TAG>\t<variant>\t<reason>"` `resolveWithSource()` in `KonfigPlugin` returns a tab-separated `"<TAG>\t<variant>\t<reason>"`
string for every dimension. This is stored as a task input (`dimensionResolutionLog`) so the task string for every dimension. This is stored as a task input (`dimensionResolutionLog`) so the task
@@ -103,7 +103,7 @@ action can emit structured lifecycle/warning/error log messages without re-runni
|---------------------------------------------|---------------------------------------------------------------------------------| |---------------------------------------------|---------------------------------------------------------------------------------|
| `-Pkonfig.buildtype=DEBUG\|RELEASE` | Forces build type; falls back to task-name detection then RELEASE | | `-Pkonfig.buildtype=DEBUG\|RELEASE` | Forces build type; falls back to task-name detection then RELEASE |
| `-Pkonfig.dimension.<name>=<variant>` | Selects a dimension variant explicitly | | `-Pkonfig.dimension.<name>=<variant>` | Selects a dimension variant explicitly |
| `-Pkonfig.force` | Disables UP-TO-DATE checks — task always re-runs (any value or bare flag works) | | `-Pkonfig.force` | Disables UP-TO-DATE checks - task always re-runs (any value or bare flag works) |
| `-Pkonfig.android.buildtypedetection=false` | Disables task-name build-type detection | | `-Pkonfig.android.buildtypedetection=false` | Disables task-name build-type detection |
| `-Pkonfig.android.flavordetection=false` | Disables task-name dimension variant detection | | `-Pkonfig.android.flavordetection=false` | Disables task-name dimension variant detection |
@@ -111,12 +111,12 @@ action can emit structured lifecycle/warning/error log messages without re-runni
| File | Role | | File | Role |
|-------------------------------|----------------------------------------------------------------------------------------------------------------| |-------------------------------|----------------------------------------------------------------------------------------------------------------|
| `KonfigPlugin.kt` | Entry point — wires providers, registers task, auto-wires source sets, hooks compile tasks | | `KonfigPlugin.kt` | Entry point - wires providers, registers task, auto-wires source sets, hooks compile tasks |
| `KonfigExtension.kt` | DSL (`konfig { }`) — top-level `field()`, `debug {}`, `release {}`, and `dimension()` functions | | `KonfigExtension.kt` | DSL (`konfig { }`) - top-level `field()`, `debug {}`, `release {}`, and `dimension()` functions |
| `DimensionConfig.kt` | DSL node for a dimension — holds variants, `common {}` block, `objectNameOverride`, `defaultVariant` | | `DimensionConfig.kt` | DSL node for a dimension - holds variants, `common {}` block, `objectNameOverride`, `defaultVariant` |
| `VariantConfig.kt` | DSL node for a variant or common block — `field()` returns `FieldHandle<T>`; `debug {}`/`release {}` supported | | `VariantConfig.kt` | DSL node for a variant or common block - `field()` returns `FieldHandle<T>`; `debug {}`/`release {}` supported |
| `FieldConfig.kt` | Single typed field — holds default `Provider<T>?` and per-`BuildType` overrides; `resolve()` returns `Provider<T>?` | | `FieldConfig.kt` | Single typed field - holds default `Provider<T>?` and per-`BuildType` overrides; `resolve()` returns `Provider<T>?` |
| `GenerateKonfigTask.kt` | `@CacheableTask` — validates inputs, logs detection results, writes the `.kt` file | | `GenerateKonfigTask.kt` | `@CacheableTask` - validates inputs, logs detection results, writes the `.kt` file |
| `BuildType.kt` | `enum` with regex-based task-name detection | | `BuildType.kt` | `enum` with regex-based task-name detection |
| `Visibility.kt` | `PUBLIC` / `INTERNAL` enum | | `Visibility.kt` | `PUBLIC` / `INTERNAL` enum |
| `KonfigDsl.kt` | `@DslMarker` annotation applied to all DSL scope classes | | `KonfigDsl.kt` | `@DslMarker` annotation applied to all DSL scope classes |
@@ -132,8 +132,8 @@ action can emit structured lifecycle/warning/error log messages without re-runni
- **Repositories:** `https://maven.bitsycore.com/releases` (primary, no auth) and - **Repositories:** `https://maven.bitsycore.com/releases` (primary, no auth) and
`https://maven.pkg.github.com/bitsycore/bitsykonfig-gradle-plugin` (fallback, needs PAT) `https://maven.pkg.github.com/bitsycore/bitsykonfig-gradle-plugin` (fallback, needs PAT)
- **JVM target:** 17 (set via `sourceCompatibility` + `KotlinCompile.compilerOptions.jvmTarget`, - **JVM target:** 17 (set via `sourceCompatibility` + `KotlinCompile.compilerOptions.jvmTarget`,
no toolchain — avoids requiring a specific JDK installation) no toolchain - avoids requiring a specific JDK installation)
- **AGP dependency:** `compileOnly("com.android.tools.build:gradle:8.7.3")` — never leaked to consumers - **AGP dependency:** `compileOnly("com.android.tools.build:gradle:8.7.3")` - never leaked to consumers
### Publishing ### Publishing
@@ -144,7 +144,7 @@ Two Maven publications are created automatically by the `kotlin-dsl` + `gradlePl
| `pluginMaven` | `konfig-gradle-plugin` | Implementation jar + sources + POM | | `pluginMaven` | `konfig-gradle-plugin` | Implementation jar + sources + POM |
| `konfigPluginMarkerMaven` | `com.bitsycore.konfig.gradle.plugin` | Marker POM that points to the impl | | `konfigPluginMarkerMaven` | `com.bitsycore.konfig.gradle.plugin` | Marker POM that points to the impl |
**Never** use `publications { create<MavenPublication>("pluginMaven") { ... } }` — this replaces **Never** use `publications { create<MavenPublication>("pluginMaven") { ... } }` - this replaces
the auto-wired publication and breaks the marker. Always configure existing publications via the auto-wired publication and breaks the marker. Always configure existing publications via
`publications.withType<MavenPublication>().configureEach { ... }`. `publications.withType<MavenPublication>().configureEach { ... }`.
@@ -175,9 +175,9 @@ Fast, no Gradle processes. Cover DSL model classes and type resolution in isolat
| File | What it covers | | File | What it covers |
|-------------------------------|------------------------------------------------------------------------| |-------------------------------|------------------------------------------------------------------------|
| `BuildTypeExtendedTest.kt` | `BuildType.resolve()` — all regex edge cases, task names, enum values | | `BuildTypeExtendedTest.kt` | `BuildType.resolve()` - all regex edge cases, task names, enum values |
| `VisibilityTest.kt` | `Visibility` enum entries, ordinals, valueOf | | `VisibilityTest.kt` | `Visibility` enum entries, ordinals, valueOf |
| `ConstantProviderTest.kt` | `constantProvider()` / `ConstantProvider` — all Provider API methods | | `ConstantProviderTest.kt` | `constantProvider()` / `ConstantProvider` - all Provider API methods |
| `FieldConfigTest.kt` | `FieldConfig` construction, default resolution, build-type overrides | | `FieldConfigTest.kt` | `FieldConfig` construction, default resolution, build-type overrides |
| `VariantConfigTest.kt` | `VariantConfig` field declarations, duplicate guard, scope blocks | | `VariantConfigTest.kt` | `VariantConfig` field declarations, duplicate guard, scope blocks |
| `DimensionConfigTest.kt` | `DimensionConfig` objectName derivation, variants, common block | | `DimensionConfigTest.kt` | `DimensionConfig` objectName derivation, variants, common block |
@@ -186,7 +186,7 @@ Fast, no Gradle processes. Cover DSL model classes and type resolution in isolat
### Functional tests (`src/functionalTest/`) ### Functional tests (`src/functionalTest/`)
Full Gradle TestKit builds — each test spins up a real Gradle project in a temp directory. Full Gradle TestKit builds - each test spins up a real Gradle project in a temp directory.
`FunctionalTestBase` provides shared helpers (`withProject`, `withFailingProject`, `generatedFile()`, `writeBuildGradle()`). `FunctionalTestBase` provides shared helpers (`withProject`, `withFailingProject`, `generatedFile()`, `writeBuildGradle()`).
| File | What it covers | | File | What it covers |
+17 -17
View File
@@ -1,6 +1,6 @@
# buildkonfig-gradle-plugin # buildkonfig-gradle-plugin
A Gradle plugin that generates a `BuildKonfig` Kotlin object at build time — like Android's `BuildConfig`, but for any Kotlin project (JVM, Multiplatform, Android). A Gradle plugin that generates a `BuildKonfig` Kotlin object at build time - like Android's `BuildConfig`, but for any Kotlin project (JVM, Multiplatform, Android).
Fields can be constant, overridden per build type (debug/release), or scoped to named **dimensions** (e.g. environment, region) with their own variants. Fields can be constant, overridden per build type (debug/release), or scoped to named **dimensions** (e.g. environment, region) with their own variants.
@@ -33,7 +33,7 @@ pluginManagement {
<details> <details>
<summary>GitHub Packages fallback (authenticated)</summary> <summary>GitHub Packages fallback (authenticated)</summary>
Store credentials in `~/.gradle/gradle.properties` — never commit them: Store credentials in `~/.gradle/gradle.properties` - never commit them:
```properties ```properties
gpr.user=YOUR_GITHUB_USERNAME gpr.user=YOUR_GITHUB_USERNAME
@@ -146,16 +146,16 @@ Three equivalent forms:
```kotlin ```kotlin
konfig { konfig {
// Fluent handle — default + one or both overrides // Fluent handle - default + one or both overrides
field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com") field("BASE_URL", "https://prod.example.com").debug("https://dev.example.com")
// Scope blocks — build type is fixed, field() returns Unit, no chaining // Scope blocks - build type is fixed, field() returns Unit, no chaining
debug { field("MOCK_API", true) } debug { field("MOCK_API", true) }
release { field("MOCK_API", false) } release { field("MOCK_API", false) }
} }
``` ```
> `field()` inside `debug {}` / `release {}` blocks intentionally returns `Unit` — the build type is already fixed by the enclosing scope, so `.debug()` / `.release()` chaining is impossible by design. > `field()` inside `debug {}` / `release {}` blocks intentionally returns `Unit` - the build type is already fixed by the enclosing scope, so `.debug()` / `.release()` chaining is impossible by design.
--- ---
@@ -239,7 +239,7 @@ public object BuildKonfig {
} }
``` ```
Root-level name collisions **fail the build** — a flat field may not shadow a Root-level name collisions **fail the build** - a flat field may not shadow a
built-in constant (`BUILD_TYPE`, `MODULE_NAME`, `IS_DEBUG`), a global field, or built-in constant (`BUILD_TYPE`, `MODULE_NAME`, `IS_DEBUG`), a global field, or
a field from another flat dimension. a field from another flat dimension.
@@ -255,7 +255,7 @@ Variants are resolved in priority order:
| 2 | `konfig.properties` file | `konfig.dimension.env=dev` | | 2 | `konfig.properties` file | `konfig.dimension.env=dev` |
| 3 | Task-name detection | Running `assembleDevDebug` matches `dev` | | 3 | Task-name detection | Running `assembleDevDebug` matches `dev` |
| 4 | `defaultTo` in DSL | `dimension("env", defaultTo = "prod")` | | 4 | `defaultTo` in DSL | `dimension("env", defaultTo = "prod")` |
| — | Omitted silently | No variant → no nested object generated | | - | Omitted silently | No variant → no nested object generated |
### `konfig.properties` file ### `konfig.properties` file
@@ -265,11 +265,11 @@ Place a `konfig.properties` file in your project directory:
konfig.dimension.env=dev konfig.dimension.env=dev
``` ```
This file is tracked as a task input — changing it invalidates the build cache. This file is tracked as a task input - changing it invalidates the build cache.
### Task-name matching rules ### Task-name matching rules
Variant detection respects **camelCase word boundaries** — a variant only Variant detection respects **camelCase word boundaries** - a variant only
matches a whole segment of the task name, never a plain substring: matches a whole segment of the task name, never a plain substring:
- `assemblePreprodRelease` matches variant `preprod`, **not** `prod` - `assemblePreprodRelease` matches variant `preprod`, **not** `prod`
@@ -283,7 +283,7 @@ Genuinely ambiguous matches are skipped with a warning.
### Selection logging ### Selection logging
The resolved build type and every dimension decision are printed by the The resolved build type and every dimension decision are printed by the
`konfigInfo` task on **every** build — including fully cached / UP-TO-DATE `konfigInfo` task on **every** build - including fully cached / UP-TO-DATE
builds with the configuration cache enabled: builds with the configuration cache enabled:
``` ```
@@ -301,7 +301,7 @@ Build type is resolved in priority order:
|----------|---------------------|-----------------------------------| |----------|---------------------|-----------------------------------|
| 1 | Explicit property | `-Pkonfig.buildtype=DEBUG` | | 1 | Explicit property | `-Pkonfig.buildtype=DEBUG` |
| 2 | Task-name detection | Running `assembleDebug` → `DEBUG` | | 2 | Task-name detection | Running `assembleDebug` → `DEBUG` |
| — | Default | `RELEASE` | | - | Default | `RELEASE` |
--- ---
@@ -311,7 +311,7 @@ Build type is resolved in priority order:
|---------------------------------------------|--------------------------------------------------| |---------------------------------------------|--------------------------------------------------|
| `-Pkonfig.buildtype=DEBUG\|RELEASE` | Forces build type | | `-Pkonfig.buildtype=DEBUG\|RELEASE` | Forces build type |
| `-Pkonfig.dimension.<name>=<variant>` | Selects a dimension variant | | `-Pkonfig.dimension.<name>=<variant>` | Selects a dimension variant |
| `-Pkonfig.force` | Disables UP-TO-DATE checks — task always re-runs | | `-Pkonfig.force` | Disables UP-TO-DATE checks - task always re-runs |
| `-Pkonfig.android.buildtypedetection=false` | Disables task-name build-type detection | | `-Pkonfig.android.buildtypedetection=false` | Disables task-name build-type detection |
| `-Pkonfig.android.flavordetection=false` | Disables task-name dimension-variant detection | | `-Pkonfig.android.flavordetection=false` | Disables task-name dimension-variant detection |
@@ -324,7 +324,7 @@ Forces `generateKonfig` to re-run on every build, bypassing Gradle's UP-TO-DATE
./gradlew assembleRelease -Pkonfig.force ./gradlew assembleRelease -Pkonfig.force
``` ```
The flag is presence-based — any value (or no value) enables it. The flag is presence-based - any value (or no value) enables it.
--- ---
@@ -344,7 +344,7 @@ println(BuildKonfig.Env.VARIANT) // "dev" or "prod"
## Build-script queries (`konfig.isDebug`, `konfig.getCurrentDimension`) ## Build-script queries (`konfig.isDebug`, `konfig.getCurrentDimension`)
The same recognition logic that drives generation is queryable from build The same recognition logic that drives generation is queryable from build
scripts — useful for wiring per-build-type dependencies in KMP projects: scripts - useful for wiring per-build-type dependencies in KMP projects:
```kotlin ```kotlin
konfig { konfig {
@@ -371,7 +371,7 @@ val activeEnv: String? = konfig.getCurrentDimension("env") // "prod", or null
## Using Gradle providers as field values ## Using Gradle providers as field values
Lazy `Provider<T>` values are supported — useful for reading Gradle properties or environment variables: Lazy `Provider<T>` values are supported - useful for reading Gradle properties or environment variables:
```kotlin ```kotlin
konfig { konfig {
@@ -380,7 +380,7 @@ konfig {
} }
``` ```
> Do not call `System.getenv()` or `project.findProperty()` directly inside `field()` — these bypass the Provider API and break configuration cache. > Do not call `System.getenv()` or `project.findProperty()` directly inside `field()` - these bypass the Provider API and break configuration cache.
--- ---
@@ -427,7 +427,7 @@ The `generateKonfig` task is automatically wired as a dependency of all `compile
# Run unit tests only # Run unit tests only
./gradlew test ./gradlew test
# Run functional tests (Gradle TestKit — starts real Gradle builds) # Run functional tests (Gradle TestKit - starts real Gradle builds)
./gradlew functionalTest ./gradlew functionalTest
# Run a specific functional test # Run a specific functional test
+1 -1
View File
@@ -75,7 +75,7 @@ publishing {
// - "pluginMaven" → the real implementation jar (groupId:artifactId:version) // - "pluginMaven" → the real implementation jar (groupId:artifactId:version)
// - "konfigPluginMarkerMaven" → the plugin marker (pluginId:pluginId.gradle.plugin:version) // - "konfigPluginMarkerMaven" → the plugin marker (pluginId:pluginId.gradle.plugin:version)
// //
// We must NOT create a third "pluginMaven" manually — that breaks the marker. // We must NOT create a third "pluginMaven" manually - that breaks the marker.
// Instead we configure the existing ones via withType. // Instead we configure the existing ones via withType.
publications.withType<MavenPublication>().configureEach { publications.withType<MavenPublication>().configureEach {
// Only decorate the implementation publication, not the marker // Only decorate the implementation publication, not the marker
@@ -7,7 +7,7 @@ import kotlin.test.assertTrue
/** /**
* Functional tests for the `common {}` block inside a dimension. * Functional tests for the `common {}` block inside a dimension.
* *
* The common block provides fallback fields for all variants — a variant field * The common block provides fallback fields for all variants - a variant field
* with the same name must take precedence over the common field. * with the same name must take precedence over the common field.
*/ */
class CommonBlockFunctionalTest : FunctionalTestBase() { class CommonBlockFunctionalTest : FunctionalTestBase() {
@@ -73,7 +73,7 @@ class DuplicateDetectionFunctionalTest : FunctionalTestBase() {
} }
@Test fun `same field name in different variants is allowed`() = withProject { dir, run -> @Test fun `same field name in different variants is allowed`() = withProject { dir, run ->
// Different variants may each define the same field name — that is the whole point // Different variants may each define the same field name - that is the whole point
dir.writeBuildGradle(""" dir.writeBuildGradle("""
plugins { id("com.bitsycore.konfig") } plugins { id("com.bitsycore.konfig") }
group = "com.example" group = "com.example"
@@ -106,7 +106,7 @@ class DuplicateDetectionFunctionalTest : FunctionalTestBase() {
assertTrue(result.output.contains("generateKonfig")) assertTrue(result.output.contains("generateKonfig"))
} }
// ── Duplicate dimension name — error message ────────────────────────────── // ── Duplicate dimension name - error message ──────────────────────────────
@Test fun `duplicate dimension error message contains dimension name`() = withFailingProject { dir, run -> @Test fun `duplicate dimension error message contains dimension name`() = withFailingProject { dir, run ->
dir.writeBuildGradle(""" dir.writeBuildGradle("""
@@ -121,7 +121,7 @@ class DuplicateDetectionFunctionalTest : FunctionalTestBase() {
assertTrue(result.output.contains("my-dim")) assertTrue(result.output.contains("my-dim"))
} }
// ── Duplicate global field — error message ──────────────────────────────── // ── Duplicate global field - error message ────────────────────────────────
@Test fun `duplicate global field error message contains field name`() = withFailingProject { dir, run -> @Test fun `duplicate global field error message contains field name`() = withFailingProject { dir, run ->
dir.writeBuildGradle(""" dir.writeBuildGradle("""
@@ -6,7 +6,7 @@ import kotlin.test.assertTrue
/** /**
* Functional tests for every supported field type emitted by the plugin: * Functional tests for every supported field type emitted by the plugin:
* String, Boolean, Int, Long, Float, Double — including correct Kotlin * String, Boolean, Int, Long, Float, Double - including correct Kotlin
* literal syntax (suffixes, special Float/Double values, escaping). * literal syntax (suffixes, special Float/Double values, escaping).
*/ */
class FieldTypesFunctionalTest : FunctionalTestBase() { class FieldTypesFunctionalTest : FunctionalTestBase() {
@@ -5,7 +5,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
/** /**
* Functional tests for `flatDimension` — fields generated at the root of the * Functional tests for `flatDimension` - fields generated at the root of the
* konfig object, with hard failure on root-level name collisions. * konfig object, with hard failure on root-level name collisions.
*/ */
class FlatDimensionFunctionalTest : FunctionalTestBase() { class FlatDimensionFunctionalTest : FunctionalTestBase() {
@@ -9,9 +9,9 @@ import kotlin.io.path.createTempDirectory
* Base class for all functional tests. * Base class for all functional tests.
* *
* Provides: * Provides:
* - [withProject] — creates a temp Gradle project, runs it, then cleans up. * - [withProject] - creates a temp Gradle project, runs it, then cleans up.
* - [File.generatedFile] — walks the output dir to find the generated `.kt` file. * - [File.generatedFile] - walks the output dir to find the generated `.kt` file.
* - [File.writeBuildGradle] — shorthand for writing a `build.gradle.kts`. * - [File.writeBuildGradle] - shorthand for writing a `build.gradle.kts`.
*/ */
abstract class FunctionalTestBase { abstract class FunctionalTestBase {
@@ -41,7 +41,7 @@ abstract class FunctionalTestBase {
/** /**
* Same as [withProject] but Gradle is invoked with `buildAndFail()` so a build * Same as [withProject] but Gradle is invoked with `buildAndFail()` so a build
* failure does NOT throw — the returned [BuildResult] carries the failed output. * failure does NOT throw - the returned [BuildResult] carries the failed output.
*/ */
protected fun withFailingProject(block: (projectDir: File, run: (List<String>) -> BuildResult) -> Unit) { protected fun withFailingProject(block: (projectDir: File, run: (List<String>) -> BuildResult) -> Unit) {
val projectDir = createTempDirectory("konfig-ft-fail").toFile() val projectDir = createTempDirectory("konfig-ft-fail").toFile()
@@ -6,7 +6,7 @@ import kotlin.test.assertTrue
/** /**
* Functional tests for top-level (global) field declarations: * Functional tests for top-level (global) field declarations:
* String, Boolean, Int — defaults, debug overrides, release overrides, * String, Boolean, Int - defaults, debug overrides, release overrides,
* and the debug/release scope block syntax. * and the debug/release scope block syntax.
*/ */
class GlobalFieldsFunctionalTest : FunctionalTestBase() { class GlobalFieldsFunctionalTest : FunctionalTestBase() {
@@ -258,7 +258,7 @@ abstract class GenerateKonfigTask : DefaultTask() {
} }
} }
/** `"my-env"` → `"MY_ENV_VARIANT"` — root constant holding the active variant of a flat dimension. */ /** `"my-env"` → `"MY_ENV_VARIANT"` - root constant holding the active variant of a flat dimension. */
private fun String.toVariantConstName(): String = private fun String.toVariantConstName(): String =
uppercase().replace(Regex("[^A-Z0-9]"), "_") + "_VARIANT" uppercase().replace(Regex("[^A-Z0-9]"), "_") + "_VARIANT"
@@ -285,7 +285,7 @@ abstract class GenerateKonfigTask : DefaultTask() {
"Long" -> """${indent}const val $name: Long = ${raw}L""" "Long" -> """${indent}const val $name: Long = ${raw}L"""
"Float" -> """${indent}const val $name: Float = ${raw.toFloat().toKotlinFloat()}""" "Float" -> """${indent}const val $name: Float = ${raw.toFloat().toKotlinFloat()}"""
"Double" -> """${indent}const val $name: Double = ${raw.toDouble().toKotlinDouble()}""" "Double" -> """${indent}const val $name: Double = ${raw.toDouble().toKotlinDouble()}"""
else -> return@forEach // unknown type — skip else -> return@forEach // unknown type - skip
} }
appendLine(line) appendLine(line)
} }
@@ -45,7 +45,7 @@ abstract class KonfigExtension @Inject constructor(
get() = objectVisibilityProp.get() get() = objectVisibilityProp.get()
set(value) = objectVisibilityProp.set(value) set(value) = objectVisibilityProp.set(value)
/** Output directory — kept as [DirectoryProperty] for full Gradle lazy semantics. */ /** Output directory - kept as [DirectoryProperty] for full Gradle lazy semantics. */
val outputDir: DirectoryProperty = objects.directoryProperty() val outputDir: DirectoryProperty = objects.directoryProperty()
// ============================================================================== // ==============================================================================
@@ -55,7 +55,7 @@ abstract class KonfigExtension @Inject constructor(
@PublishedApi @PublishedApi
internal val dimensions: MutableList<DimensionConfig> = mutableListOf() internal val dimensions: MutableList<DimensionConfig> = mutableListOf()
/** Backing store for global fields — reuses [com.bitsycore.konfig.configs.VariantConfig] for its field/debug/release logic. */ /** Backing store for global fields - reuses [com.bitsycore.konfig.configs.VariantConfig] for its field/debug/release logic. */
@PublishedApi @PublishedApi
internal val globalScope: VariantConfig = VariantConfig("\$global") internal val globalScope: VariantConfig = VariantConfig("\$global")
@@ -117,10 +117,10 @@ abstract class KonfigExtension @Inject constructor(
// MARK: Build-script queries // MARK: Build-script queries
// ============================================================================== // ==============================================================================
/** Wired by the plugin at apply time — same resolution chain as the generated object. */ /** Wired by the plugin at apply time - same resolution chain as the generated object. */
internal lateinit var buildTypeProviderInternal: Provider<BuildType> internal lateinit var buildTypeProviderInternal: Provider<BuildType>
/** Wired by the plugin at apply time — resolves a dimension with the same recognition logic. */ /** Wired by the plugin at apply time - resolves a dimension with the same recognition logic. */
internal lateinit var dimensionResolverInternal: (String) -> Provider<String> internal lateinit var dimensionResolverInternal: (String) -> Provider<String>
/** Resolved build type as a lazy provider (`"debug"` / `"release"`). */ /** Resolved build type as a lazy provider (`"debug"` / `"release"`). */
@@ -132,7 +132,7 @@ abstract class KonfigExtension @Inject constructor(
get() = buildTypeProviderInternal.map { it == BuildType.DEBUG } get() = buildTypeProviderInternal.map { it == BuildType.DEBUG }
/** /**
* True when the resolved build type is debug — uses the exact same recognition * True when the resolved build type is debug - uses the exact same recognition
* as the generated object, so it can drive build-script decisions such as * as the generated object, so it can drive build-script decisions such as
* KMP `debugImplementation`-style wiring: * KMP `debugImplementation`-style wiring:
* *
@@ -14,7 +14,7 @@ import org.gradle.work.DisableCachingByDefault
* [GenerateKonfigTask] only logs when it actually executes, so a cached / * [GenerateKonfigTask] only logs when it actually executes, so a cached /
* up-to-date build would silently hide which variant is active. This task is * up-to-date build would silently hide which variant is active. This task is
* untracked (never up-to-date, never cached) and `generateKonfig` depends on * untracked (never up-to-date, never cached) and `generateKonfig` depends on
* it, guaranteeing the selection is always visible in the logs — even on a * it, guaranteeing the selection is always visible in the logs - even on a
* fully cached build with the configuration cache enabled. * fully cached build with the configuration cache enabled.
*/ */
@DisableCachingByDefault(because = "pure logging task, must run on every build") @DisableCachingByDefault(because = "pure logging task, must run on every build")
@@ -293,11 +293,11 @@ class KonfigPlugin : Plugin<Project> {
* Resolves a dimension and encodes the result as a tab-separated string for logging. * Resolves a dimension and encodes the result as a tab-separated string for logging.
* *
* Format: `"<TAG>\t<variant>\t<reason>"` * Format: `"<TAG>\t<variant>\t<reason>"`
* - TAG = `OK` — active, variant resolved successfully * - TAG = `OK` - active, variant resolved successfully
* - TAG = `WARN_UNKNOWN` — property set but value is not a known variant * - TAG = `WARN_UNKNOWN` - property set but value is not a known variant
* - TAG = `WARN_AMBIGUOUS` — multiple variants matched task names * - TAG = `WARN_AMBIGUOUS` - multiple variants matched task names
* - TAG = `SKIP` — no active variant could be determined * - TAG = `SKIP` - no active variant could be determined
* - TAG = `ERROR` — configuration error (e.g. invalid defaultTo) * - TAG = `ERROR` - configuration error (e.g. invalid defaultTo)
*/ */
private fun resolveWithSource( private fun resolveWithSource(
dim: DimensionConfig, dim: DimensionConfig,
@@ -11,7 +11,7 @@ import java.util.function.BiFunction
* *
* Values are stored as [Provider]<T> so lazy sources (e.g. Gradle property providers) * Values are stored as [Provider]<T> so lazy sources (e.g. Gradle property providers)
* are supported alongside plain constants. [org.gradle.api.provider.ProviderFactory] * are supported alongside plain constants. [org.gradle.api.provider.ProviderFactory]
* is intentionally NOT stored here — it is not configuration-cache serializable and * is intentionally NOT stored here - it is not configuration-cache serializable and
* must never flow into the object graph captured by task input providers. * must never flow into the object graph captured by task input providers.
*/ */
class FieldConfig<T : Any> @PublishedApi internal constructor( class FieldConfig<T : Any> @PublishedApi internal constructor(
@@ -16,7 +16,7 @@ import org.gradle.api.provider.Provider
* field("TIMEOUT", 30).debug(5) * field("TIMEOUT", 30).debug(5)
* field("URL", "https://prod.example.com").debug("https://dev.example.com").release("https://prod.example.com") * field("URL", "https://prod.example.com").debug("https://dev.example.com").release("https://prod.example.com")
* ``` * ```
* Not available inside `debug {}` / `release {}` blocks — those return [Unit]. * Not available inside `debug {}` / `release {}` blocks - those return [Unit].
*/ */
class FieldHandle<T : Any> @PublishedApi internal constructor( class FieldHandle<T : Any> @PublishedApi internal constructor(
@PublishedApi internal val field: FieldConfig<T> @PublishedApi internal val field: FieldConfig<T>
@@ -34,7 +34,7 @@ class FieldHandle<T : Any> @PublishedApi internal constructor(
/** /**
* Receiver of `debug { ... }` and `release { ... }` blocks inside [VariantConfig]. * Receiver of `debug { ... }` and `release { ... }` blocks inside [VariantConfig].
* *
* `field()` here returns [Unit] — no `.debug()`/`.release()` chaining is possible * `field()` here returns [Unit] - no `.debug()`/`.release()` chaining is possible
* because the build type is already fixed by the enclosing scope. * because the build type is already fixed by the enclosing scope.
*/ */
@KonfigDsl @KonfigDsl
@@ -128,20 +128,20 @@ class BuildTypeExtendedTest {
} }
@Test fun `prereleased does not resolve RELEASE`() { @Test fun `prereleased does not resolve RELEASE`() {
// "release" is preceded by lowercase 'e' in "prere[lease]" — but "release" // "release" is preceded by lowercase 'e' in "prere[lease]" - but "release"
// starts after "prere", let's verify actual behaviour via the regex: // starts after "prere", let's verify actual behaviour via the regex:
// lookbehind (?<![a-z]) fails because char before 'r' of "release" is 'e' // lookbehind (?<![a-z]) fails because char before 'r' of "release" is 'e'
assertNull(BuildType.resolve("prereleased")) assertNull(BuildType.resolve("prereleased"))
} }
@Test fun `released does not resolve RELEASE`() { @Test fun `released does not resolve RELEASE`() {
// "released" — 'd' after "release" is NOT a lowercase letter... wait, // "released" - 'd' after "release" is NOT a lowercase letter... wait,
// actually the regex checks (?![a-z]) so 'd' fails the lookahead. // actually the regex checks (?![a-z]) so 'd' fails the lookahead.
assertNull(BuildType.resolve("released")) assertNull(BuildType.resolve("released"))
} }
@Test fun `debugMode resolves DEBUG`() { @Test fun `debugMode resolves DEBUG`() {
// 'M' after "debug" — uppercase, not [a-z], so lookahead passes // 'M' after "debug" - uppercase, not [a-z], so lookahead passes
assertEquals(BuildType.DEBUG, BuildType.resolve("debugMode")) assertEquals(BuildType.DEBUG, BuildType.resolve("debugMode"))
} }
@@ -119,7 +119,7 @@ class DimensionConfigTest {
d.variant("prod") { field("URL", "https://prod.example.com") } d.variant("prod") { field("URL", "https://prod.example.com") }
d.variant("prod") { field("KEY", "secret") } d.variant("prod") { field("KEY", "secret") }
val v = d.variants["prod"]!! val v = d.variants["prod"]!!
// Same VariantConfig instance reused — two fields total // Same VariantConfig instance reused - two fields total
assertEquals(2, v.fields.size) assertEquals(2, v.fields.size)
} }
@@ -115,7 +115,7 @@ class FieldHandleTest {
@Test fun `handle with no debug or release set returns null for scope-only field`() { @Test fun `handle with no debug or release set returns null for scope-only field`() {
val fc = FieldConfig<String>("F", String::class.java, null) val fc = FieldConfig<String>("F", String::class.java, null)
@Suppress("UNUSED_VARIABLE") val handle = FieldHandle(fc) @Suppress("UNUSED_VARIABLE") val handle = FieldHandle(fc)
// No overrides set — resolve returns null // No overrides set - resolve returns null
assertNull(fc.resolve(BuildType.DEBUG)) assertNull(fc.resolve(BuildType.DEBUG))
assertNull(fc.resolve(BuildType.RELEASE)) assertNull(fc.resolve(BuildType.RELEASE))
} }
@@ -33,7 +33,7 @@ class TaskNameMatchingTest {
@Test fun `camelCase variant matches its camelCase segment`() { @Test fun `camelCase variant matches its camelCase segment`() {
assertTrue("assemblePreProdRelease".containsWordCamelCase("preProd")) assertTrue("assemblePreProdRelease".containsWordCamelCase("preProd"))
// "Prod" is a legitimate camelCase segment inside PreProd — the resolver's // "Prod" is a legitimate camelCase segment inside PreProd - the resolver's
// longest-match rule (tested functionally) disambiguates this case. // longest-match rule (tested functionally) disambiguates this case.
assertTrue("assemblePreProdRelease".containsWordCamelCase("prod")) assertTrue("assemblePreProdRelease".containsWordCamelCase("prod"))
} }