From 324ce139cf066d18bbaf2946d5b0178e7cbc7662 Mon Sep 17 00:00:00 2001
From: Janosch Hildebrand <jnosh+github@jnosh.com>
Date: Wed, 29 Apr 2020 22:24:09 +0200
Subject: [PATCH] feat(gradle): gradle plugin dependency versions in variables
 (#6063) (#6064)

* feat(gradle): gradle plugin dependency versions in variables (#6063)

* chore: Replace uses of RegExp with util/regEx in build-gradle.ts

Co-authored-by: Jamie Magee <JamieMagee@users.noreply.github.com>
---
 lib/manager/gradle/build-gradle.spec.ts | 83 ++++++++++++++++++++++++
 lib/manager/gradle/build-gradle.ts      | 86 +++++++++++++++++--------
 2 files changed, 143 insertions(+), 26 deletions(-)

diff --git a/lib/manager/gradle/build-gradle.spec.ts b/lib/manager/gradle/build-gradle.spec.ts
index a8d0b3c5ff..14c68fca52 100644
--- a/lib/manager/gradle/build-gradle.spec.ts
+++ b/lib/manager/gradle/build-gradle.spec.ts
@@ -494,6 +494,70 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns an updated file if the plugin version defined in a variable is found', () => {
+    const gradleFile = `val mysqlVersion = "6.0.5"
+               id("mysql") 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"
+               id("mysql") version mysqlVersion
+               `
+    );
+  });
+
+  it('returns an updated file if the plugin version defined in a variable in a template string without curly braces is found', () => {
+    const gradleFile = `val mysqlVersion = "6.0.5"
+               id("mysql") 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"
+               id("mysql") version "$mysqlVersion"
+               `
+    );
+  });
+
+  it('returns an updated file if the plugin version defined in a variable in a template string with curly braces is found', () => {
+    const gradleFile = `val mysqlVersion = "6.0.5"
+               id("mysql") 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"
+               id("mysql") version "\${mysqlVersion}"
+               `
+    );
+  });
+
+  it('returns an updated file if the plugin version defined in a variable in a template string with triple double quotes is found', () => {
+    const gradleFile = `String mysqlVersion = "6.0.5"
+               id 'mysql' version """$mysqlVersion"""
+               `;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `String mysqlVersion = "7.0.0"
+               id 'mysql' 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
@@ -706,4 +770,23 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
     expect(updatedGradleFile).toEqual('val mysqlVersion by extra { "7.0.0" }');
   });
+
+  it('should replace a external variable assigned to a plugin dependency', () => {
+    const gradleFile = 'id("mysql") version "$mysqlVersion";';
+    const mysqlDependency = {
+      group: 'mysql',
+      depGroup: 'mysql',
+      name: 'mysql-connector-java',
+      version: '6.0.5',
+    };
+    collectVersionVariables([mysqlDependency], gradleFile);
+
+    const gradleWithVersionFile = 'String mysqlVersion = "6.0.5"';
+    const updatedGradleFile = updateGradleVersion(
+      gradleWithVersionFile,
+      mysqlDependency,
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual('String mysqlVersion = "7.0.0"');
+  });
 });
diff --git a/lib/manager/gradle/build-gradle.ts b/lib/manager/gradle/build-gradle.ts
index 78b4b22f78..c9813adaab 100644
--- a/lib/manager/gradle/build-gradle.ts
+++ b/lib/manager/gradle/build-gradle.ts
@@ -1,4 +1,5 @@
 import { BuildDependency } from './gradle-updates-report';
+import { regEx } from '../../util/regex';
 
 /**
  * Functions adapted/ported from https://github.com/patrikerdes/gradle-use-latest-versions-plugin
@@ -23,10 +24,12 @@ interface UpdateFunction {
 }
 
 const groovyQuotes = `(?:["'](?:""|'')?)`;
+const groovyVersionVariable = `(?:${groovyQuotes}\\$)?{?([^\\s"'{}$)]+)}?${groovyQuotes}?`;
+const kotlinVersionVariable = `(?:"\\$)?{?([^\\s"{}$]+?)}?"?`;
 
 // https://github.com/patrikerdes/gradle-use-latest-versions-plugin/blob/8cf9c3917b8b04ba41038923cab270d2adda3aa6/src/main/groovy/se/patrikerdes/DependencyUpdate.groovy#L27-L29
 function moduleStringVersionFormatMatch(dependency: GradleDependency): RegExp {
-  return new RegExp(
+  return regEx(
     `(${groovyQuotes}${dependency.group}:${dependency.name}:)[^$].*?(([:@].*?)?${groovyQuotes})`
   );
 }
@@ -34,17 +37,15 @@ function moduleStringVersionFormatMatch(dependency: GradleDependency): RegExp {
 function groovyPluginStringVersionFormatMatch(
   dependency: GradleDependency
 ): RegExp {
-  return new RegExp(
-    `(id\\s+${groovyQuotes}${dependency.group}${groovyQuotes}\\s+version\\s+${groovyQuotes})[^$].*?(${groovyQuotes})`
+  return regEx(
+    `(id\\s+${groovyQuotes}${dependency.group}${groovyQuotes}\\s+version\\s+${groovyQuotes})[^"$].*?(${groovyQuotes})`
   );
 }
 
 function kotlinPluginStringVersionFormatMatch(
   dependency: GradleDependency
 ): RegExp {
-  return new RegExp(
-    `(id\\("${dependency.group}"\\)\\s+version\\s+")[^$].*?(")`
-  );
+  return regEx(`(id\\("${dependency.group}"\\)\\s+version\\s+")[^$].*?(")`);
 }
 
 function allMapFormatOrders(
@@ -62,7 +63,7 @@ function allMapFormatOrders(
     `${version}${comma}${group}${comma}${name}`,
     `${name}${comma}${version}${comma}${group}`,
     `${version}${comma}${name}${comma}${group}`,
-  ].map((regex) => new RegExp(`${prefix}${regex}${postfix}`));
+  ].map((regex) => regEx(`${prefix}${regex}${postfix}`));
 }
 
 function moduleMapVersionFormatMatch(dependency: GradleDependency): RegExp[] {
@@ -89,7 +90,7 @@ function moduleMapVariableVersionFormatMatch(
   // one capture group: the version variable
   const group = `group\\s*:\\s*${groovyQuotes}${dependency.group}${groovyQuotes}`;
   const name = `name\\s*:\\s*${groovyQuotes}${dependency.name}${groovyQuotes}`;
-  const version = `version\\s*:\\s*(?:${groovyQuotes}\\$)?{?([^\\s"'{}$)]+)}?${groovyQuotes}?`;
+  const version = `version\\s*:\\s*${groovyVersionVariable}`;
   return allMapFormatOrders(group, name, version, '', '');
 }
 
@@ -99,14 +100,14 @@ function moduleKotlinNamedArgumentVariableVersionFormatMatch(
   // one capture group: the version variable
   const group = `group\\s*=\\s*"${dependency.group}"`;
   const name = `name\\s*=\\s*"${dependency.name}"`;
-  const version = `version\\s*=\\s*(?:"\\$)?{?([^\\s"{}$]+?)}?"?`;
+  const version = `version\\s*=\\s*${kotlinVersionVariable}`;
   return allMapFormatOrders(group, name, version, '', '[\\s),]');
 }
 
 function moduleStringVariableInterpolationVersionFormatMatch(
   dependency: GradleDependency
 ): RegExp {
-  return new RegExp(
+  return regEx(
     `${groovyQuotes}${dependency.group}:${dependency.name}:\\$([^{].*?)${groovyQuotes}`
   );
 }
@@ -114,21 +115,37 @@ function moduleStringVariableInterpolationVersionFormatMatch(
 function moduleStringVariableExpressionVersionFormatMatch(
   dependency: GradleDependency
 ): RegExp {
-  return new RegExp(
+  return regEx(
     `${groovyQuotes}${dependency.group}:${dependency.name}:\\$` +
       `{([^{].*?)}${groovyQuotes}`
   );
 }
 
+function groovyPluginVariableVersionFormatMatch(
+  dependency: GradleDependency
+): RegExp {
+  return regEx(
+    `id\\s+${groovyQuotes}${dependency.group}${groovyQuotes}\\s+version\\s+${groovyVersionVariable}(?:\\s|;|})`
+  );
+}
+
+function kotlinPluginVariableVersionFormatMatch(
+  dependency: GradleDependency
+): RegExp {
+  return regEx(
+    `id\\("${dependency.group}"\\)\\s+version\\s+${kotlinVersionVariable}(?:\\s|;|})`
+  );
+}
+
 function variableDefinitionFormatMatch(variable: string): RegExp {
-  return new RegExp(`(${variable}\\s*=\\s*?["'])(.*)(["'])`);
+  return regEx(`(${variable}\\s*=\\s*?["'])(.*)(["'])`);
 }
 
 function variableMapDefinitionFormatMatch(
   variable: string,
   version: string
 ): RegExp {
-  return new RegExp(`(${variable}\\s*:\\s*?["'])(${version})(["'])`);
+  return regEx(`(${variable}\\s*:\\s*?["'])(${version})(["'])`);
 }
 
 export function collectVersionVariables(
@@ -143,6 +160,8 @@ export function collectVersionVariables(
     const regexes = [
       moduleStringVariableExpressionVersionFormatMatch(dependency),
       moduleStringVariableInterpolationVersionFormatMatch(dependency),
+      groovyPluginVariableVersionFormatMatch(dependency),
+      kotlinPluginVariableVersionFormatMatch(dependency),
       ...moduleMapVariableVersionFormatMatch(dependency),
       ...moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
     ];
@@ -173,8 +192,12 @@ function updateVersionLiterals(
     ...moduleKotlinNamedArgumentVersionFormatMatch(dependency),
   ];
   for (const regex of regexes) {
-    if (regex.test(buildGradleContent)) {
-      return buildGradleContent.replace(regex, `$1${newVersion}$2`);
+    const match = regex.exec(buildGradleContent);
+    if (match) {
+      return buildGradleContent.replace(
+        match[0],
+        `${match[1]}${newVersion}${match[2]}`
+      );
     }
   }
   return null;
@@ -189,15 +212,23 @@ function updateLocalVariables(
     ...moduleMapVariableVersionFormatMatch(dependency),
     moduleStringVariableInterpolationVersionFormatMatch(dependency),
     moduleStringVariableExpressionVersionFormatMatch(dependency),
+    groovyPluginVariableVersionFormatMatch(dependency),
+    kotlinPluginVariableVersionFormatMatch(dependency),
     ...moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
   ];
   for (const regex of regexes) {
     const match = regex.exec(buildGradleContent);
     if (match) {
-      return buildGradleContent.replace(
-        variableDefinitionFormatMatch(match[1]),
-        `$1${newVersion}$3`
+      const variableDefinitionRegex = variableDefinitionFormatMatch(match[1]);
+      const variableDefinitionMatch = variableDefinitionRegex.exec(
+        buildGradleContent
       );
+      if (variableDefinitionMatch) {
+        return buildGradleContent.replace(
+          variableDefinitionMatch[0],
+          `${variableDefinitionMatch[1]}${newVersion}${variableDefinitionMatch[3]}`
+        );
+      }
     }
   }
   return null;
@@ -214,8 +245,8 @@ function updateGlobalVariables(
     const match = regex.exec(buildGradleContent);
     if (match) {
       return buildGradleContent.replace(
-        variableDefinitionFormatMatch(variable),
-        `$1${newVersion}$3`
+        match[0],
+        `${match[1]}${newVersion}${match[3]}`
       );
     }
   }
@@ -237,8 +268,8 @@ function updateGlobalMapVariables(
       const match = regex.exec(buildGradleContent);
       if (match) {
         return buildGradleContent.replace(
-          variableMapDefinitionFormatMatch(variable, dependency.version),
-          `$1${newVersion}$3`
+          match[0],
+          `${match[1]}${newVersion}${match[3]}`
         );
       }
 
@@ -256,12 +287,15 @@ function updateKotlinVariablesByExtra(
 ): string | null {
   const variable = variables[`${dependency.group}:${dependency.name}`];
   if (variable) {
-    const regex = new RegExp(
+    const regex = regEx(
       `(val ${variable} by extra(?: {|\\()\\s*")(.*)("\\s*[})])`
     );
     const match = regex.exec(buildGradleContent);
     if (match) {
-      return buildGradleContent.replace(regex, `$1${newVersion}$3`);
+      return buildGradleContent.replace(
+        match[0],
+        `${match[1]}${newVersion}${match[3]}`
+      );
     }
   }
   return null;
@@ -274,10 +308,10 @@ function updatePropertyFileGlobalVariables(
 ): string | null {
   const variable = variables[`${dependency.group}:${dependency.name}`];
   if (variable) {
-    const regex = new RegExp(`(${variable}\\s*=\\s*)(.*)`);
+    const regex = regEx(`(${variable}\\s*=\\s*)(.*)`);
     const match = regex.exec(buildGradleContent);
     if (match) {
-      return buildGradleContent.replace(regex, `$1${newVersion}`);
+      return buildGradleContent.replace(match[0], `${match[1]}${newVersion}`);
     }
   }
   return null;
-- 
GitLab