diff --git a/.devpack-for-spring/.gitignore b/.devpack-for-spring/.gitignore
new file mode 100644
index 0000000..9515f83
--- /dev/null
+++ b/.devpack-for-spring/.gitignore
@@ -0,0 +1 @@
+devpack-for-spring-cli
\ No newline at end of file
diff --git a/.devpack-for-spring/plugin-configuration.yaml b/.devpack-for-spring/plugin-configuration.yaml
new file mode 100644
index 0000000..4c62fee
--- /dev/null
+++ b/.devpack-for-spring/plugin-configuration.yaml
@@ -0,0 +1,617 @@
+format:
+ gradle:
+ id: io.spring.javaformat
+ version: 0.0.47
+ default-task: format
+ description: Format the source code
+ tasks:
+ format: format
+ repository: gradlePluginPortal()
+ maven:
+ id: io.spring.javaformat:spring-javaformat-maven-plugin
+ version: 0.0.47
+ default-task: format
+ description: Format the source code
+ tasks:
+ format: :apply
+dependencies:
+ gradle:
+ id: io.github.rockcrafters.rockcraft
+ version: 1.2.4
+ repository: gradlePluginPortal()
+ default-task: dependencies-export
+ description: |
+ Save project dependencies
+ tasks:
+ dependencies-export: dependencies-export
+ maven:
+ id: io.github.rockcrafters:rockcraft-maven-plugin
+ version: 1.2.4
+ default-task: dependencies-export
+ description: |
+ Save project dependencies
+ tasks:
+ dependencies-export: :create-build-rock
+rockcraft:
+ gradle:
+ id: io.github.rockcrafters.rockcraft
+ version: 1.2.4
+ repository: gradlePluginPortal()
+ default-task: build-rock
+ description: |
+ Plugin for rock image generation
+ tasks:
+ create-rock: create-rock
+ build-rock: build-rock
+ create-build-rock: create-build-rock
+ build-build-rock: build-build-rock
+ push-rock: push-rock
+ push-build-rock: push-build-rock
+ maven:
+ id: io.github.rockcrafters:rockcraft-maven-plugin
+ version: 1.2.4
+ default-task: build-rock
+ description: |
+ Plugin for rock image generation
+ tasks:
+ create-rock:
+ - install
+ - :create-rock
+ build-rock:
+ - install
+ - :create-rock
+ - :build-rock
+ create-build-rock:
+ - :create-build-rock
+ push-rock:
+ - install
+ - :create-rock
+ - :build-rock
+ - :push-rock
+ push-build-rock:
+ - install
+ - :create-build-rock
+ - :build-build-rock
+ - :push-build-rock
+checkstyleGoogle:
+ resources:
+ - path: .config/checkstyle/checkstyle.xml
+ content: |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ gradle:
+ id: checkstyle
+ default-task: checkstyle
+ configuration:
+ gradleGroovy: |
+ checkstyle {
+ toolVersion = '13.3.0'
+ configDirectory = file("${rootProject.projectDir}/.config/checkstyle")
+ }
+ dependencies {
+ checkstyle 'com.puppycrawl.tools:checkstyle:13.3.0'
+ }
+ gradleKotlin: |
+ checkstyle {
+ toolVersion = "13.3.0"
+ configDirectory = file("${rootProject.projectDir}/.config/checkstyle")
+ }
+ dependencies {
+ checkstyle("com.puppycrawl.tools:checkstyle:13.3.0")
+ }
+ tasks:
+ checkstyle:
+ - checkStyleMain
+ - checkStyleTest
+ checkstyleMain:
+ - checkstyleMain
+ checkstyleTest:
+ - checkstyleTest
+ maven:
+ id: org.apache.maven.plugins:maven-checkstyle-plugin
+ version: 3.6.0
+ default-task: check
+ tasks:
+ check: checkstyle:check
+ configuration:
+ maven:
+ configuration: |
+
+ ${project.basedir}/.config/checkstyle/checkstyle.xml
+
+ dependencies: |
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 13.3.0
+
+
+ executions: |
+
+
+ checkstyle
+ validate
+
+ check
+
+
+
diff --git a/.github/workflows/pr-self-hosted.yml b/.github/workflows/pr-self-hosted.yml
index 4341437..9b45a30 100644
--- a/.github/workflows/pr-self-hosted.yml
+++ b/.github/workflows/pr-self-hosted.yml
@@ -28,7 +28,6 @@ jobs:
rm -rf /home/runner/.local/state/rockcraft/log/*
sudo snap install rockcraft --classic
- run: |
- export LOGGING_LEVEL_COM_CANONICAL_DEVPACKSPRING_PROCESSUTIL=DEBUG
./gradlew build -PexcludeTags=boot,maven-modification,maven-dependency --no-daemon --stacktrace
- name: Store reports
if: failure()
diff --git a/build.gradle b/build.gradle
index 1723578..38530a4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -221,3 +221,7 @@ if (useAot) {
}
}
+
+bootJar {
+ requiresUnpack '**/kotlin-compiler-embeddable-*.jar'
+}
diff --git a/src/main/java/com/canonical/devpackspring/ConfigUtil.java b/src/main/java/com/canonical/devpackspring/ConfigUtil.java
index edf983d..034fdbc 100644
--- a/src/main/java/com/canonical/devpackspring/ConfigUtil.java
+++ b/src/main/java/com/canonical/devpackspring/ConfigUtil.java
@@ -33,13 +33,31 @@ public abstract class ConfigUtil {
private static final Log LOG = LogFactory.getLog(ConfigUtil.class);
/**
- * Opens configuration file stream
+ * Opens configuration file stream from: 1. System Property 2. Environment Variable 3.
+ * Current directory .devpack-for-spring/conffile 4. User home
+ * .config/devpack-for-spring/conffile 5. Embedded resource
+ * /com/canonical/devpackspring/conffile
* @param environment - environment variable specifying path to the file
* @param fileName - configuration file name
* @return configuration file InputStream
* @throws FileNotFoundException - configuration file not found
*/
public static InputStream openConfigurationFile(String environment, String fileName) throws FileNotFoundException {
+
+ String pluginConfigurationFile = System.getProperty(environment);
+ if (pluginConfigurationFile == null) {
+ pluginConfigurationFile = System.getenv(environment);
+ }
+ if (pluginConfigurationFile != null) {
+ if (Files.exists(Path.of(pluginConfigurationFile))) {
+ LOG.info("Reading configuration from " + pluginConfigurationFile);
+ return new FileInputStream(pluginConfigurationFile);
+ }
+ else {
+ LOG.warn("Configuration file " + environment + "=" + pluginConfigurationFile + " does not exist.");
+ }
+ }
+
Path currentConfigPath = Path.of(System.getProperty("user.dir"))
.resolve(".devpack-for-spring")
.resolve(fileName);
@@ -57,19 +75,6 @@ public static InputStream openConfigurationFile(String environment, String fileN
return new FileInputStream(configPath.toFile());
}
- String pluginConfigurationFile = System.getenv(environment);
- if (pluginConfigurationFile == null) {
- pluginConfigurationFile = System.getProperty(environment);
- }
- if (pluginConfigurationFile != null) {
- if (Files.exists(Path.of(pluginConfigurationFile))) {
- LOG.info("Reading configuration from " + pluginConfigurationFile);
- return new FileInputStream(pluginConfigurationFile);
- }
- else {
- LOG.warn("Configuration file " + environment + "=" + pluginConfigurationFile + " does not exist.");
- }
- }
LOG.info("Reading default configuration " + fileName);
return ConfigUtil.class.getResourceAsStream(String.format("/com/canonical/devpackspring/%s", fileName));
}
diff --git a/src/main/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipe.java b/src/main/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipe.java
index a3f8226..2888242 100644
--- a/src/main/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipe.java
+++ b/src/main/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipe.java
@@ -127,6 +127,16 @@ private boolean addOrReplace(List targetStatements, Statement configS
for (int i = 0; i < targetStatements.size(); i++) {
Statement targetStmt = targetStatements.get(i);
if (matches(targetStmt, configStmt)) {
+ // Special case: merge dependencies{} blocks instead of replacing
+ if (isDependenciesBlock(targetStmt)) {
+ Statement merged = DependencyMergeUtil.mergeDependenciesBlock(targetStmt, configStmt, targetCu,
+ configCu);
+ if (merged != null) {
+ targetStatements.set(i, merged);
+ return true;
+ }
+ return false;
+ }
// avoid modifying if the trimmed outputs are strictly identical
org.openrewrite.Cursor targetCursor = new org.openrewrite.Cursor(
new org.openrewrite.Cursor(null, targetCu), targetStmt);
@@ -142,6 +152,10 @@ private boolean addOrReplace(List targetStatements, Statement configS
return true;
}
+ private boolean isDependenciesBlock(Statement stmt) {
+ return stmt instanceof J.MethodInvocation m && "dependencies".equals(m.getSimpleName());
+ }
+
private boolean matches(Statement targetStmt, Statement configStmt) {
// handle project.ext.set -> we want to override only properties with the same
// name
diff --git a/src/main/java/com/canonical/devpackspring/rewrite/DependencyMergeUtil.java b/src/main/java/com/canonical/devpackspring/rewrite/DependencyMergeUtil.java
new file mode 100644
index 0000000..285da94
--- /dev/null
+++ b/src/main/java/com/canonical/devpackspring/rewrite/DependencyMergeUtil.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.canonical.devpackspring.rewrite;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+import org.openrewrite.Cursor;
+import org.openrewrite.SourceFile;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.Statement;
+
+public abstract class DependencyMergeUtil {
+
+ public static Statement mergeDependenciesBlock(Statement targetStmt, Statement configStmt, SourceFile targetCu,
+ SourceFile configCu) {
+ J.MethodInvocation targetMethod = (J.MethodInvocation) targetStmt;
+ J.MethodInvocation configMethod = (J.MethodInvocation) configStmt;
+
+ List targetDeps = getDependenciesStatements(targetMethod);
+ List configDeps = getDependenciesStatements(configMethod);
+ if (targetDeps == null) {
+ return configStmt;
+ }
+ if (configDeps == null) {
+ return null; // nothing to merge, skip it
+ }
+
+ HashSet deps = new HashSet<>();
+ HashMap target = new HashMap<>();
+ HashMap source = new HashMap<>();
+ targetDeps.forEach(x -> updateDependencies(x, deps, target));
+ configDeps.forEach(x -> updateDependencies(x, deps, source));
+
+ List mergedDeps = new ArrayList<>();
+ var sortedList = new ArrayList<>(deps);
+ Collections.sort(sortedList);
+ for (var key : sortedList) {
+ Statement stm = source.get(key);
+ if (stm != null) {
+ if (!mergedDeps.isEmpty()) {
+ stm = stm.withPrefix(mergedDeps.getLast().getPrefix());
+ }
+ mergedDeps.add(stm);
+ continue;
+ }
+ stm = target.get(key);
+ if (stm != null) {
+ if (!mergedDeps.isEmpty()) {
+ stm = stm.withPrefix(mergedDeps.getLast().getPrefix());
+ }
+ mergedDeps.add(stm);
+ }
+ }
+ Statement rebuilt = rebuildDependenciesStatements(targetMethod, mergedDeps);
+ if (rebuilt == null) {
+ return null;
+ }
+ Cursor rc = new Cursor(new Cursor(null, targetCu), rebuilt);
+ String newResult = rebuilt.printTrimmed(rc).trim();
+ Cursor cc = new Cursor(new Cursor(null, configCu), targetStmt);
+ String oldResult = targetStmt.printTrimmed(cc).trim();
+ if (oldResult.equals(newResult)) {
+ return null;
+ }
+ return rebuilt;
+ }
+
+ private static void updateDependencies(Statement stm, HashSet deps, HashMap depMap) {
+ var key = getDependencyKey(stm);
+ if (key == null) {
+ throw new RuntimeException("Unexpected element in depends block " + stm.toString());
+ }
+ deps.add(key);
+ depMap.put(key, stm);
+ }
+
+ private static List getDependenciesStatements(J.MethodInvocation m) {
+ if (m.getArguments().size() == 1) {
+ org.openrewrite.java.tree.Expression arg = m.getArguments().getFirst();
+ if (arg instanceof J.Lambda lambda && lambda.getBody() instanceof J.Block block) {
+ return block.getStatements().stream().map(stmt -> {
+ if (stmt instanceof J.Return ret) {
+ return (Statement) ret.getExpression();
+ }
+ return stmt;
+ }).toList();
+ }
+ }
+ return null;
+ }
+
+ private static Statement rebuildDependenciesStatements(J.MethodInvocation m, List newStatements) {
+ if (m.getArguments().size() == 1) {
+ org.openrewrite.java.tree.Expression arg = m.getArguments().getFirst();
+ if (arg instanceof J.Lambda lambda && lambda.getBody() instanceof J.Block block) {
+ J.Block newBlock = block.withStatements(newStatements.stream().map(stmt -> {
+ String ws = stmt.getPrefix().getWhitespace();
+ if (!ws.contains("\n")) {
+ stmt = stmt.withPrefix(stmt.getPrefix().withWhitespace("\n" + ws));
+ }
+ return stmt;
+ }).toList());
+ J.Lambda newLambda = lambda.withBody(newBlock);
+ return m.withArguments(List.of(newLambda));
+ }
+ }
+ return null;
+ }
+
+ private static String getDependencyKey(Statement stmt) {
+ if (!(stmt instanceof J.MethodInvocation m)) {
+ return null;
+ }
+ String scope = m.getSimpleName();
+ if (m.getArguments().isEmpty()) {
+ return null;
+ }
+ String coord = m.getArguments().getFirst().toString();
+ // Strip the version segment: "group:artifact:version" -> "group:artifact"
+ String[] parts = coord.split(":");
+ if (parts.length >= 2) {
+ return scope + ":" + parts[0] + ":" + parts[1];
+ }
+ if (parts.length == 1) {
+ return scope + ":" + coord;
+ }
+ throw new RuntimeException("Unexpected statement " + stmt);
+ }
+
+}
diff --git a/src/main/java/com/canonical/devpackspring/rewrite/visitors/KotlinAddPluginVisitor.java b/src/main/java/com/canonical/devpackspring/rewrite/visitors/KotlinAddPluginVisitor.java
index a42582a..b74ff70 100644
--- a/src/main/java/com/canonical/devpackspring/rewrite/visitors/KotlinAddPluginVisitor.java
+++ b/src/main/java/com/canonical/devpackspring/rewrite/visitors/KotlinAddPluginVisitor.java
@@ -20,6 +20,8 @@
import java.util.List;
import com.canonical.devpackspring.rewrite.StatementUtil;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.NonNull;
import org.openrewrite.ExecutionContext;
import org.openrewrite.InMemoryExecutionContext;
@@ -31,9 +33,12 @@
import org.openrewrite.kotlin.KotlinIsoVisitor;
import org.openrewrite.kotlin.KotlinParser;
import org.openrewrite.kotlin.tree.K;
+import org.openrewrite.tree.ParseError;
public class KotlinAddPluginVisitor extends KotlinIsoVisitor {
+ private static final Log LOG = LogFactory.getLog(KotlinAddPluginVisitor.class);
+
private final String pluginTemplateKotlin = "plugins {\n\tid(\"%s\") version \"%s\"\n}\n";
private final String builtInTemplateKotlin = "plugins {\n\tid(\"%s\")\n}\n";
@@ -56,7 +61,10 @@ public KotlinAddPluginVisitor(String pluginName, String pluginVersion) {
Paths.get("/tmp"), context)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Could not parse as Gradle Kotlin"));
-
+ if (templateSource instanceof ParseError error) {
+ LOG.error("Unable to parse: " + pluginDefinition);
+ throw new RuntimeException("Parser Error:" + error.printAll());
+ }
List statements = ((K.CompilationUnit) templateSource).getStatements();
J.Block block = (J.Block) statements.get(0);
K.MethodInvocation stm = (K.MethodInvocation) block.getStatements().get(0);
diff --git a/src/test/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipeTests.java b/src/test/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipeTests.java
index 38620b0..ff22c6b 100644
--- a/src/test/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipeTests.java
+++ b/src/test/java/com/canonical/devpackspring/rewrite/AddConfigurationRecipeTests.java
@@ -330,4 +330,92 @@ void testKotlinConfigurationAppendProperty() {
"""));
}
+ @Test
+ void testGroovyDependenciesMerge() {
+ String config = """
+ dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter:3.5.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
+ }
+ """;
+ G.CompilationUnit cu = parseGroovyConfig(config);
+ rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, false)), Assertions.buildGradle("""
+ dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter:3.3.0'
+ runtimeOnly 'org.postgresql:postgresql:42.7.0'
+ }
+ """, """
+ dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter:3.5.0'
+ runtimeOnly 'org.postgresql:postgresql:42.7.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
+ }
+ """));
+ rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, false)), Assertions.buildGradle("""
+ dependencies {
+ }
+ """, """
+ dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter:3.5.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
+ }
+ """));
+ }
+
+ @Test
+ void testGroovyDependenciesMergeNewLines() {
+ String config = """
+ dependencies {
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
+ }
+ """;
+ G.CompilationUnit cu = parseGroovyConfig(config);
+ rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, false)), Assertions.buildGradle("""
+ dependencies { implementation 'org.springframework.boot:spring-boot-starter:3.3.0' }
+ """, """
+ dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter:3.3.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' }
+ """));
+ }
+
+ @Test
+ void testKotlinDependenciesMergeNewLines() {
+ String config = """
+ dependencies {
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
+ }
+ """;
+ K.CompilationUnit cu = parseKotlinConfig(config);
+ rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, true)), Assertions.buildGradleKts("""
+ dependencies { implementation("org.springframework.boot:spring-boot-starter:3.3.0") }
+ """, """
+ dependencies {
+ implementation("org.springframework.boot:spring-boot-starter:3.3.0")
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") }"""));
+ }
+
+ @Test
+ void testKotlinDependenciesMerge() {
+ String config = """
+ dependencies {
+ implementation("org.springframework.boot:spring-boot-starter:3.5.0")
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
+ }
+ """;
+ K.CompilationUnit cu = parseKotlinConfig(config);
+ rewriteRun(spec -> spec.recipe(new AddConfigurationRecipe(cu, true)), Assertions.buildGradleKts("""
+ dependencies {
+ implementation("org.springframework.boot:spring-boot-starter:3.3.0")
+ runtimeOnly("org.postgresql:postgresql:42.7.0")
+ }
+ """, """
+ dependencies {
+ implementation("org.springframework.boot:spring-boot-starter:3.5.0")
+ runtimeOnly("org.postgresql:postgresql:42.7.0")
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.0")
+ }
+ """));
+ }
+
}
diff --git a/src/test/java/com/canonical/devpackspring/rewrite/AddGradlePluginRecipeTests.java b/src/test/java/com/canonical/devpackspring/rewrite/AddGradlePluginRecipeTests.java
index cc04e7f..37a0998 100644
--- a/src/test/java/com/canonical/devpackspring/rewrite/AddGradlePluginRecipeTests.java
+++ b/src/test/java/com/canonical/devpackspring/rewrite/AddGradlePluginRecipeTests.java
@@ -158,4 +158,23 @@ void testKotlinAppendPlugin() {
"""));
}
+ @Test
+ void testKotlinAppendDefaultPlugin() {
+ rewriteRun(spec -> spec.recipe(new AddGradlePluginRecipe("checkstyle", null, true)),
+ Assertions.buildGradleKts("""
+ plugins {
+ kotlin("jvm") version "1.9.22"
+ }
+ group = "com.example"
+ version = "1.0"
+ """, """
+ plugins {
+ kotlin("jvm") version "1.9.22"
+ id("checkstyle")
+ }
+ group = "com.example"
+ version = "1.0"
+ """));
+ }
+
}
diff --git a/test-data/projects/gradle-kotlin/build.gradle.kts b/test-data/projects/gradle-kotlin/build.gradle.kts
index c25b77f..e28cbed 100644
--- a/test-data/projects/gradle-kotlin/build.gradle.kts
+++ b/test-data/projects/gradle-kotlin/build.gradle.kts
@@ -9,7 +9,7 @@ version = "0.0.1-SNAPSHOT"
java {
toolchain {
- languageVersion = JavaLanguageVersion.of(17)
+ languageVersion = JavaLanguageVersion.of(25)
}
}