From 9b2f4576cd185e6f44592ba2e3c72bfdcbe2f664 Mon Sep 17 00:00:00 2001
From: IKEDA Sho <suicaicoca@gmail.com>
Date: Sun, 4 Aug 2019 20:18:05 +0900
Subject: [PATCH] feat(gradle): Add basic Gradle Kotlin DSL support (#4086)

---
 lib/config/definitions.js                     |  2 +-
 lib/manager/gradle/build-gradle.ts            | 36 ++++++++-
 lib/manager/gradle/index.ts                   |  8 +-
 renovate-schema.json                          |  2 +-
 .../gradle/__snapshots__/index.spec.ts.snap   | 79 +++++++++++++++++++
 test/manager/gradle/build-gradle.spec.ts      | 58 ++++++++++++++
 test/manager/gradle/index.spec.ts             | 37 +++++++++
 website/docs/java.md                          |  4 +-
 8 files changed, 218 insertions(+), 8 deletions(-)

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index e524139c62..e6ff3a1819 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -1644,7 +1644,7 @@ const options = [
     stage: 'package',
     type: 'object',
     default: {
-      fileMatch: ['\\.gradle$', '(^|/)gradle.properties$'],
+      fileMatch: ['\\.gradle(\\.kts)?$', '(^|/)gradle.properties$'],
       timeout: 300,
       versionScheme: 'maven',
     },
diff --git a/lib/manager/gradle/build-gradle.ts b/lib/manager/gradle/build-gradle.ts
index 4174fa7b9d..4322fbe793 100644
--- a/lib/manager/gradle/build-gradle.ts
+++ b/lib/manager/gradle/build-gradle.ts
@@ -64,6 +64,7 @@ export function collectVersionVariables(
       moduleStringVariableExpressionVersionFormatMatch(dependency),
       moduleStringVariableInterpolationVersionFormatMatch(dependency),
       moduleMapVariableVersionFormatMatch(dependency),
+      moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
     ];
 
     for (const regex of regexes) {
@@ -86,8 +87,10 @@ function updateVersionLiterals(
 ) {
   const regexes: RegExp[] = [
     moduleStringVersionFormatMatch(dependency),
-    pluginStringVersionFormatMatch(dependency),
+    groovyPluginStringVersionFormatMatch(dependency),
+    kotlinPluginStringVersionFormatMatch(dependency),
     moduleMapVersionFormatMatch(dependency),
+    moduleKotlinNamedArgumentVersionFormatMatch(dependency),
   ];
   for (const regex of regexes) {
     if (buildGradleContent.match(regex)) {
@@ -106,6 +109,7 @@ function updateLocalVariables(
     moduleMapVariableVersionFormatMatch(dependency),
     moduleStringVariableInterpolationVersionFormatMatch(dependency),
     moduleStringVariableExpressionVersionFormatMatch(dependency),
+    moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
   ];
   for (const regex of regexes) {
     const match = buildGradleContent.match(regex);
@@ -161,12 +165,18 @@ function moduleStringVersionFormatMatch(dependency: GradleDependency) {
   );
 }
 
-function pluginStringVersionFormatMatch(dependency: GradleDependency) {
+function groovyPluginStringVersionFormatMatch(dependency: GradleDependency) {
   return new RegExp(
     `(id\\s+["']${dependency.group}["']\\s+version\\s+["'])[^$].*?(["'])`
   );
 }
 
+function kotlinPluginStringVersionFormatMatch(dependency: GradleDependency) {
+  return new RegExp(
+    `(id\\("${dependency.group}"\\)\\s+version\\s+")[^$].*?(")`
+  );
+}
+
 function moduleMapVersionFormatMatch(dependency: GradleDependency) {
   // prettier-ignore
   return new RegExp(
@@ -176,6 +186,17 @@ function moduleMapVersionFormatMatch(dependency: GradleDependency) {
   );
 }
 
+function moduleKotlinNamedArgumentVersionFormatMatch(
+  dependency: GradleDependency
+) {
+  // prettier-ignore
+  return new RegExp(
+    `(group\\s*=\\s*"${dependency.group}"\\s*,\\s*` +
+    `name\\s*=\\s*"${dependency.name}"\\s*,\\s*` +
+    `version\\s*=\\s*").*?(")`
+  );
+}
+
 function moduleMapVariableVersionFormatMatch(dependency: GradleDependency) {
   // prettier-ignore
   return new RegExp(
@@ -185,6 +206,17 @@ function moduleMapVariableVersionFormatMatch(dependency: GradleDependency) {
   );
 }
 
+function moduleKotlinNamedArgumentVariableVersionFormatMatch(
+  dependency: GradleDependency
+) {
+  // prettier-ignore
+  return new RegExp(
+    `group\\s*=\\s*"${dependency.group}"\\s*,\\s*` +
+    `name\\s*=\\s*"${dependency.name}"\\s*,\\s*` +
+    `version\\s*=\\s*([^\\s"]+?)[\\s\\),]`
+  );
+}
+
 function moduleStringVariableInterpolationVersionFormatMatch(
   dependency: GradleDependency
 ) {
diff --git a/lib/manager/gradle/index.ts b/lib/manager/gradle/index.ts
index 353255b4bc..fd3a572de5 100644
--- a/lib/manager/gradle/index.ts
+++ b/lib/manager/gradle/index.ts
@@ -22,8 +22,12 @@ export async function extractAllPackageFiles(
   config: ExtractConfig,
   packageFiles: string[]
 ): Promise<PackageFile[]> {
-  if (!packageFiles.some(packageFile => packageFile === 'build.gradle')) {
-    logger.warn('No root build.gradle found - skipping');
+  if (
+    !packageFiles.some(packageFile =>
+      ['build.gradle', 'build.gradle.kts'].includes(packageFile)
+    )
+  ) {
+    logger.warn('No root build.gradle nor build.gradle.kts found - skipping');
     return null;
   }
   logger.info('Extracting dependencies from all gradle files');
diff --git a/renovate-schema.json b/renovate-schema.json
index 09c4387cc8..7559eb8d61 100644
--- a/renovate-schema.json
+++ b/renovate-schema.json
@@ -1130,7 +1130,7 @@
       "description": "Configuration object for build.gradle files",
       "type": "object",
       "default": {
-        "fileMatch": ["\\.gradle$", "(^|/)gradle.properties$"],
+        "fileMatch": ["\\.gradle(\\.kts)?$", "(^|/)gradle.properties$"],
         "timeout": 300,
         "versionScheme": "maven"
       },
diff --git a/test/manager/gradle/__snapshots__/index.spec.ts.snap b/test/manager/gradle/__snapshots__/index.spec.ts.snap
index be187adcdc..c375181170 100644
--- a/test/manager/gradle/__snapshots__/index.spec.ts.snap
+++ b/test/manager/gradle/__snapshots__/index.spec.ts.snap
@@ -79,6 +79,85 @@ Array [
 ]
 `;
 
+exports[`manager/gradle extractPackageFile should return gradle.kts dependencies 1`] = `
+Array [
+  Object {
+    "datasource": "maven",
+    "deps": Array [
+      Object {
+        "currentValue": null,
+        "depGroup": "org.springframework.boot",
+        "depName": "org.springframework.boot:spring-boot-starter-jersey",
+        "name": "spring-boot-starter-jersey",
+        "registryUrls": Array [
+          "https://repo.maven.apache.org/maven2/",
+          "https://jitpack.io",
+        ],
+      },
+      Object {
+        "currentValue": "1.0-groovy-2.4",
+        "depGroup": "org.spockframework",
+        "depName": "org.spockframework:spock-core",
+        "name": "spock-core",
+        "registryUrls": Array [
+          "https://repo.maven.apache.org/maven2/",
+          "https://jitpack.io",
+        ],
+      },
+      Object {
+        "currentValue": "3.1",
+        "depGroup": "cglib",
+        "depName": "cglib:cglib-nodep",
+        "name": "cglib-nodep",
+        "registryUrls": Array [
+          "https://repo.maven.apache.org/maven2/",
+          "https://jitpack.io",
+        ],
+      },
+    ],
+    "manager": "gradle",
+    "packageFile": "build.gradle.kts",
+  },
+  Object {
+    "datasource": "maven",
+    "deps": Array [
+      Object {
+        "currentValue": null,
+        "depGroup": "org.springframework.boot",
+        "depName": "org.springframework.boot:spring-boot-starter-jersey",
+        "name": "spring-boot-starter-jersey",
+        "registryUrls": Array [
+          "https://repo.maven.apache.org/maven2/",
+          "https://jitpack.io",
+        ],
+      },
+      Object {
+        "currentValue": "1.0-groovy-2.4",
+        "depGroup": "org.spockframework",
+        "depName": "org.spockframework:spock-core",
+        "name": "spock-core",
+        "registryUrls": Array [
+          "https://repo.maven.apache.org/maven2/",
+          "https://jitpack.io",
+        ],
+      },
+      Object {
+        "currentValue": "3.1",
+        "depGroup": "cglib",
+        "depName": "cglib:cglib-nodep",
+        "name": "cglib-nodep",
+        "registryUrls": Array [
+          "https://repo.maven.apache.org/maven2/",
+          "https://jitpack.io",
+        ],
+      },
+    ],
+    "manager": "gradle",
+    "packageFile": "subproject/build.gradle.kts",
+  },
+]
+`;
+
 exports[`manager/gradle extractPackageFile should throw registry failure if gradle execution fails 1`] = `[Error: registry-failure]`;
 
 exports[`manager/gradle extractPackageFile should use repositories only for current project 1`] = `
diff --git a/test/manager/gradle/build-gradle.spec.ts b/test/manager/gradle/build-gradle.spec.ts
index da7d7c86cf..ae058cde60 100644
--- a/test/manager/gradle/build-gradle.spec.ts
+++ b/test/manager/gradle/build-gradle.spec.ts
@@ -82,6 +82,22 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns a file updated if the version defined as a Kotlin named argument is found', () => {
+    const gradleFile = `compile(group   = "mysql"               ,
+               name    = "mysql-connector-java",
+               version = "6.0.5")`;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `compile(group   = "mysql"               ,
+               name    = "mysql-connector-java",
+               version = "7.0.0")`
+    );
+  });
+
   it('should returns a file updated if the version defined in a variable as a string is found', () => {
     const gradleFile = `String mysqlVersion= "6.0.5"
     runtime (  "mysql:mysql-connector-java:$mysqlVersion"  )
@@ -130,6 +146,26 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('should returns a file updated if the version defined in a variable as a Kotlin named argument is found', () => {
+    const gradleFile = `val mysqlVersion = "6.0.5"
+               compile(group = "mysql"               ,
+               name          = "mysql-connector-java",
+               version       = mysqlVersion)
+               `;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `val mysqlVersion = "7.0.0"
+               compile(group = "mysql"               ,
+               name          = "mysql-connector-java",
+               version       = mysqlVersion)
+               `
+    );
+  });
+
   it('should replace a external groovy variable assigned to a specific dependency', () => {
     const gradleFile =
       'runtime (  "mysql:mysql-connector-java:${mysqlVersion}"  )'; // eslint-disable-line no-template-curly-in-string
@@ -192,6 +228,28 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     expect(updatedGradleFile).toEqual('String mysqlVersion = "7.0.0"');
   });
 
+  it('should replace a external variable assigned to a Kotlin named argument dependency', () => {
+    const gradleFile = `compile(group  = "mysql"               ,
+               name           = "mysql-connector-java",
+               version        = mysqlVersion)
+               `;
+    const mysqlDependency = {
+      group: 'mysql',
+      depGroup: 'mysql',
+      name: 'mysql-connector-java',
+      version: '6.0.5',
+    };
+    collectVersionVariables([mysqlDependency], gradleFile);
+
+    const gradleWithVersionFile = 'val mysqlVersion = "6.0.5"';
+    const updatedGradleFile = updateGradleVersion(
+      gradleWithVersionFile,
+      mysqlDependency,
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual('val mysqlVersion = "7.0.0"');
+  });
+
   it('should replace a external variable assigned to a interpolated dependency', () => {
     const gradleFile =
       'runtime (  "mysql:mysql-connector-java:$mysqlVersion"  )';
diff --git a/test/manager/gradle/index.spec.ts b/test/manager/gradle/index.spec.ts
index 81a2ca3fe3..88b3074d99 100644
--- a/test/manager/gradle/index.spec.ts
+++ b/test/manager/gradle/index.spec.ts
@@ -42,6 +42,14 @@ describe('manager/gradle', () => {
       expect(dependencies).toMatchSnapshot();
     });
 
+    it('should return gradle.kts dependencies', async () => {
+      const dependencies = await manager.extractAllPackageFiles(config, [
+        'build.gradle.kts',
+        'subproject/build.gradle.kts',
+      ]);
+      expect(dependencies).toMatchSnapshot();
+    });
+
     it('should return empty if there are no dependencies', async () => {
       fs.readFile.mockResolvedValue(fsReal.readFileSync(
         'test/datasource/gradle/_fixtures/updatesReportEmpty.json',
@@ -201,5 +209,34 @@ describe('manager/gradle', () => {
         'id "com.github.ben-manes.versions" version "0.20.0"'
       );
     });
+
+    it('should update an existing plugin dependency with Kotlin DSL', () => {
+      const buildGradleContent = `
+        plugins {
+            id("com.github.ben-manes.versions") version "0.20.0"
+        }
+        `;
+      const upgrade = {
+        depGroup: 'com.github.ben-manes.versions',
+        name: 'com.github.ben-manes.versions.gradle.plugin',
+        version: '0.20.0',
+        newValue: '0.21.0',
+      };
+      const buildGradleContentUpdated = manager.updateDependency(
+        buildGradleContent,
+        upgrade
+      );
+
+      expect(buildGradleContent).not.toMatch(
+        'id("com.github.ben-manes.versions") version "0.21.0"'
+      );
+
+      expect(buildGradleContentUpdated).toMatch(
+        'id("com.github.ben-manes.versions") version "0.21.0"'
+      );
+      expect(buildGradleContentUpdated).not.toMatch(
+        'id("com.github.ben-manes.versions") version "0.20.0"'
+      );
+    });
   });
 });
diff --git a/website/docs/java.md b/website/docs/java.md
index 36618ad8da..87be83fdca 100644
--- a/website/docs/java.md
+++ b/website/docs/java.md
@@ -13,11 +13,11 @@ Renovate detects versions specified as string `'group:artifact:version'` and as
 
 ### File Support
 
-Renovate can update `build.gradle` files in the root of the repository and any `*.gradle` file inside any subdirectory as multi-project configurations.
+Renovate can update `build.gradle`/`build.gradle.kts` files in the root of the repository and any `*.gradle`/`*.gradle.kts` file inside any subdirectory as multi-project configurations.
 
 Renovate does not support:
 
-- Projects without a `build.gradle` file in the root of the repository.
+- Projects without neither `build.gradle` nor `build.gradle.kts` file in the root of the repository.
 - Android projects that requires extra configuration to run. (e.g. setting the android SDK)
 
 ### How It Works
-- 
GitLab