From 5928562064bd843670777756c28001df227d021b Mon Sep 17 00:00:00 2001
From: Joshua Gleitze <dev@joshuagleitze.de>
Date: Sun, 26 Jan 2020 09:18:29 +0200
Subject: [PATCH] =?UTF-8?q?feat(gradle):=20Support=20any=20Order=20in=20Gr?=
 =?UTF-8?q?adle=E2=80=99s=20Map=20Syntax=20(#5196)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/manager/gradle/build-gradle.ts       |  82 +++++++-----
 test/manager/gradle/build-gradle.spec.ts | 160 +++++++++++++++++++++++
 2 files changed, 208 insertions(+), 34 deletions(-)

diff --git a/lib/manager/gradle/build-gradle.ts b/lib/manager/gradle/build-gradle.ts
index 7445665dcd..787a6977f5 100644
--- a/lib/manager/gradle/build-gradle.ts
+++ b/lib/manager/gradle/build-gradle.ts
@@ -47,46 +47,60 @@ function kotlinPluginStringVersionFormatMatch(
   );
 }
 
-function moduleMapVersionFormatMatch(dependency: GradleDependency): RegExp {
-  // prettier-ignore
-  return new RegExp(
-    `(group\\s*:\\s*${groovyQuotes}${dependency.group}${groovyQuotes}\\s*,\\s*` +
-    `name\\s*:\\s*${groovyQuotes}${dependency.name}${groovyQuotes}\\s*,\\s*` +
-    `version\\s*:\\s*${groovyQuotes})[^{}$"']+?(${groovyQuotes})`
-  );
+function allMapFormatOrders(
+  group: string,
+  name: string,
+  version: string,
+  prefix: string,
+  postfix: string
+): RegExp[] {
+  const comma = '\\s*,\\s*';
+  return [
+    `${group}${comma}${name}${comma}${version}`,
+    `${group}${comma}${version}${comma}${name}`,
+    `${name}${comma}${group}${comma}${version}`,
+    `${version}${comma}${group}${comma}${name}`,
+    `${name}${comma}${version}${comma}${group}`,
+    `${version}${comma}${name}${comma}${group}`,
+  ].map(regex => new RegExp(`${prefix}${regex}${postfix}`));
+}
+
+function moduleMapVersionFormatMatch(dependency: GradleDependency): RegExp[] {
+  // two captures groups: start and end. The version is in between them
+  const group = `group\\s*:\\s*${groovyQuotes}${dependency.group}${groovyQuotes}`;
+  const name = `name\\s*:\\s*${groovyQuotes}${dependency.name}${groovyQuotes}`;
+  const version = `version\\s*:\\s*${groovyQuotes})[^{}$"']+?(${groovyQuotes}`;
+  return allMapFormatOrders(group, name, version, '(', ')');
 }
 
 function moduleKotlinNamedArgumentVersionFormatMatch(
   dependency: GradleDependency
-): RegExp {
-  // prettier-ignore
-  return new RegExp(
-    `(group\\s*=\\s*"${dependency.group}"\\s*,\\s*` +
-    `name\\s*=\\s*"${dependency.name}"\\s*,\\s*` +
-    `version\\s*=\\s*")[^{}$]*?(")`
-  );
+): RegExp[] {
+  // two captures groups: start and end. The version is in between them
+  const group = `group\\s*=\\s*"${dependency.group}"`;
+  const name = `name\\s*=\\s*"${dependency.name}"`;
+  const version = `version\\s*=\\s*")[^{}$]*?("`;
+  return allMapFormatOrders(group, name, version, '(', ')');
 }
 
 function moduleMapVariableVersionFormatMatch(
   dependency: GradleDependency
-): RegExp {
-  // prettier-ignore
-  return new RegExp(
-    `group\\s*:\\s*${groovyQuotes}${dependency.group}${groovyQuotes}\\s*,\\s*` +
-    `name\\s*:\\s*${groovyQuotes}${dependency.name}${groovyQuotes}\\s*,\\s*` +
-    `version\\s*:\\s*(?:${groovyQuotes}\\$)?{?([^\\s"'{}$)]+)}?${groovyQuotes}?\\s*`
-  );
+): RegExp[] {
+  // 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}?`;
+  return allMapFormatOrders(group, name, version, '', '');
 }
 
 function moduleKotlinNamedArgumentVariableVersionFormatMatch(
   dependency: GradleDependency
-): RegExp {
-  // prettier-ignore
-  return new RegExp(
-    `group\\s*=\\s*"${dependency.group}"\\s*,\\s*` +
-    `name\\s*=\\s*"${dependency.name}"\\s*,\\s*` +
-    `version\\s*=\\s*(?:"\\$)?{?([^\\s"{}$]+?)}?"?[\\s\\),]`
-  );
+): RegExp[] {
+  // 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"{}$]+?)}?"?`;
+  return allMapFormatOrders(group, name, version, '', '[\\s),]');
 }
 
 function moduleStringVariableInterpolationVersionFormatMatch(
@@ -121,8 +135,8 @@ export function collectVersionVariables(
     const regexes = [
       moduleStringVariableExpressionVersionFormatMatch(dependency),
       moduleStringVariableInterpolationVersionFormatMatch(dependency),
-      moduleMapVariableVersionFormatMatch(dependency),
-      moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
+      ...moduleMapVariableVersionFormatMatch(dependency),
+      ...moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
     ];
 
     for (const regex of regexes) {
@@ -147,8 +161,8 @@ function updateVersionLiterals(
     moduleStringVersionFormatMatch(dependency),
     groovyPluginStringVersionFormatMatch(dependency),
     kotlinPluginStringVersionFormatMatch(dependency),
-    moduleMapVersionFormatMatch(dependency),
-    moduleKotlinNamedArgumentVersionFormatMatch(dependency),
+    ...moduleMapVersionFormatMatch(dependency),
+    ...moduleKotlinNamedArgumentVersionFormatMatch(dependency),
   ];
   for (const regex of regexes) {
     if (buildGradleContent.match(regex)) {
@@ -164,10 +178,10 @@ function updateLocalVariables(
   newVersion: string
 ): string | null {
   const regexes: RegExp[] = [
-    moduleMapVariableVersionFormatMatch(dependency),
+    ...moduleMapVariableVersionFormatMatch(dependency),
     moduleStringVariableInterpolationVersionFormatMatch(dependency),
     moduleStringVariableExpressionVersionFormatMatch(dependency),
-    moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
+    ...moduleKotlinNamedArgumentVariableVersionFormatMatch(dependency),
   ];
   for (const regex of regexes) {
     const match = buildGradleContent.match(regex);
diff --git a/test/manager/gradle/build-gradle.spec.ts b/test/manager/gradle/build-gradle.spec.ts
index eb4b9ab885..3bc3114329 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 an updated file if the version in single quotes defined as map is found in a different order', () => {
+    const gradleFile = `compile name : 'mysql-connector-java',
+               group  : 'mysql',
+               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 name : 'mysql-connector-java',
+               group  : 'mysql',
+               version: '7.0.0'`
+    );
+  });
+
   it('returns an updated file if the version in double quotes defined as map is found', () => {
     const gradleFile = `compile group  : 'mysql'               ,
                name   : 'mysql-connector-java',
@@ -98,6 +114,22 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns an updated file if the version in double quotes defined as map is found in a different order', () => {
+    const gradleFile = `compile name   : 'mysql-connector-java',
+               version: "6.0.5",
+               group  : 'mysql'`;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `compile name   : 'mysql-connector-java',
+               version: "7.0.0",
+               group  : 'mysql'`
+    );
+  });
+
   it('returns an updated file if the version in triple single quotes defined as map is found', () => {
     const gradleFile = `compile group  : 'mysql'               ,
                name   : 'mysql-connector-java',
@@ -114,6 +146,22 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns an updated file if the version in triple single quotes defined as map is found in a different order', () => {
+    const gradleFile = `compile version: '''6.0.5''',
+               group  : 'mysql',
+               name   : 'mysql-connector-java'`;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `compile version: '''7.0.0''',
+               group  : 'mysql',
+               name   : 'mysql-connector-java'`
+    );
+  });
+
   it('returns an updated file if the version in triple double quotes defined as map is found', () => {
     const gradleFile = `compile group  : 'mysql'               ,
                name   : 'mysql-connector-java',
@@ -130,6 +178,22 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns an updated file if the version in triple double quotes defined as map is found in a different order', () => {
+    const gradleFile = `compile version: """6.0.5""",
+               name   : 'mysql-connector-java',
+               group  : 'mysql'`;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `compile version: """7.0.0""",
+               name   : 'mysql-connector-java',
+               group  : 'mysql'`
+    );
+  });
+
   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",
@@ -146,6 +210,22 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns a file updated if the version defined as a Kotlin named argument is found in a different order', () => {
+    const gradleFile = `compile(group = "mysql",
+               version = "6.0.5",
+               name    = "mysql-connector-java")`;
+    const updatedGradleFile = updateGradleVersion(
+      gradleFile,
+      { group: 'mysql', name: 'mysql-connector-java', version: '6.0.5' },
+      '7.0.0'
+    );
+    expect(updatedGradleFile).toEqual(
+      `compile(group = "mysql",
+               version = "7.0.0",
+               name    = "mysql-connector-java")`
+    );
+  });
+
   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"  )
@@ -194,6 +274,26 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('should returns a file updated if the version defined in a variable as a map is found in a different order', () => {
+    const gradleFile = `String mysqlVersion = "6.0.5"
+               compile name   : 'mysql-connector-java',
+               group          : '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"
+               compile name   : 'mysql-connector-java',
+               group          : 'mysql'               ,
+               version        : mysqlVersion
+               `
+    );
+  });
+
   it('returns an updated file if the version defined in a variable in a simple template string without curly braces as a map is found', () => {
     const gradleFile = `String mysqlVersion = "6.0.5"
                compile group  : 'mysql'               ,
@@ -234,6 +334,26 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns an updated file if the version defined in a variable in a simple template string with curly braces as a map is found in a different order', () => {
+    const gradleFile = `String mysqlVersion = "6.0.5"
+               compile name   : 'mysql-connector-java',
+               version        : "\${mysqlVersion}",
+               group          : 'mysql'
+               `;
+    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"
+               compile name   : 'mysql-connector-java',
+               version        : "\${mysqlVersion}",
+               group          : 'mysql'
+               `
+    );
+  });
+
   it('returns an updated file if the version defined in a variable in a triple template string without curly braces as a map is found', () => {
     const gradleFile = `String mysqlVersion = "6.0.5"
                compile group  : 'mysql'               ,
@@ -294,6 +414,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 in a different order', () => {
+    const gradleFile = `val mysqlVersion = "6.0.5"
+               compile(name  = "mysql-connector-java",
+               group         = "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"
+               compile(name  = "mysql-connector-java",
+               group         = "mysql"               ,
+               version       = mysqlVersion)
+               `
+    );
+  });
+
   it('returns an updated file if the version defined in a variable in a template string without curly braces as a Kotlin named argument is found', () => {
     const gradleFile = `val mysqlVersion = "6.0.5"
                compile(group = "mysql"               ,
@@ -334,6 +474,26 @@ describe('lib/manager/gradle/updateGradleVersion', () => {
     );
   });
 
+  it('returns an updated file if the version defined in a variable in a template string with curly braces as a Kotlin named argument is found in a different order', () => {
+    const gradleFile = `val mysqlVersion = "6.0.5"
+               compile(version = "\${mysqlVersion}"    ,
+               name            = "mysql-connector-java",
+               group           = "mysql")
+               `;
+    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(version = "\${mysqlVersion}"    ,
+               name            = "mysql-connector-java",
+               group           = "mysql")
+               `
+    );
+  });
+
   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
-- 
GitLab