From d9349b21b97b9d770c116efac25e57b851f99e84 Mon Sep 17 00:00:00 2001
From: Johannes Feichtner <Churro@users.noreply.github.com>
Date: Tue, 27 Dec 2022 14:45:30 +0100
Subject: [PATCH] refactor(manager/gradle): group extraction tests + code
 hygiene (#19586)

---
 lib/modules/manager/gradle/extract.spec.ts    | 1595 ++++++++---------
 lib/modules/manager/gradle/extract.ts         |   55 +-
 lib/modules/manager/gradle/parser.ts          |    4 +-
 .../manager/gradle/parser/common.spec.ts      |    2 +-
 lib/modules/manager/gradle/parser/handlers.ts |    6 +-
 lib/modules/manager/gradle/types.ts           |    2 +-
 6 files changed, 802 insertions(+), 862 deletions(-)

diff --git a/lib/modules/manager/gradle/extract.spec.ts b/lib/modules/manager/gradle/extract.spec.ts
index 7f09d81329..249c959102 100644
--- a/lib/modules/manager/gradle/extract.spec.ts
+++ b/lib/modules/manager/gradle/extract.spec.ts
@@ -1,4 +1,4 @@
-import { stripIndent } from 'common-tags';
+import { codeBlock, stripIndent } from 'common-tags';
 import { Fixtures } from '../../../../test/fixtures';
 import { fs, logger } from '../../../../test/util';
 import type { ExtractConfig } from '../types';
@@ -35,17 +35,15 @@ describe('modules/manager/gradle/extract', () => {
   });
 
   it('returns null', async () => {
-    mockFs({
+    const fsMock = {
       'gradle.properties': '',
       'build.gradle': '',
-    });
-
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-      'gradle.properties',
-    ]);
+    };
+    mockFs(fsMock);
 
-    expect(res).toBeNull();
+    expect(
+      await extractAllPackageFiles({} as ExtractConfig, Object.keys(fsMock))
+    ).toBeNull();
   });
 
   it('logs a warning in case parseGradle throws an exception', async () => {
@@ -63,35 +61,17 @@ describe('modules/manager/gradle/extract', () => {
     );
   });
 
-  it('extracts from cross-referenced files', async () => {
-    mockFs({
-      'gradle.properties': 'baz=1.2.3',
-      'build.gradle': 'url "https://example.com"; "foo:bar:$baz"',
-    });
-
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-      'gradle.properties',
-    ]);
-
-    expect(res).toMatchObject([
-      {
-        packageFile: 'gradle.properties',
-        deps: [{ depName: 'foo:bar', currentValue: '1.2.3' }],
-      },
-      { packageFile: 'build.gradle', deps: [] },
-    ]);
-  });
-
   it('skips versions composed from multiple variables', async () => {
-    mockFs({
+    const fsMock = {
       'build.gradle':
         'foo = "1"; bar = "2"; baz = "3"; "foo:bar:$foo.$bar.$baz"',
-    });
+    };
+    mockFs(fsMock);
 
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-    ]);
+    const res = await extractAllPackageFiles(
+      {} as ExtractConfig,
+      Object.keys(fsMock)
+    );
 
     expect(res).toMatchObject([
       {
@@ -100,30 +80,25 @@ describe('modules/manager/gradle/extract', () => {
           {
             depName: 'foo:bar',
             currentValue: '1.2.3',
-            registryUrls: [],
             skipReason: 'contains-variable',
-            managerData: {
-              packageFile: 'build.gradle',
-            },
           },
         ],
       },
     ]);
   });
 
-  it('works with file-ext-var', async () => {
-    mockFs({
+  it('extracts from cross-referenced files', async () => {
+    const fsMock = {
       'gradle.properties': 'baz=1.2.3',
       'build.gradle':
         'repositories { maven { url "https://example.com" } }; "foo:bar:$baz@zip"',
-      'settings.gradle': null as never, // TODO: #7154
-    });
+    };
+    mockFs(fsMock);
 
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-      'gradle.properties',
-      'settings.gradle',
-    ]);
+    const res = await extractAllPackageFiles(
+      {} as ExtractConfig,
+      Object.keys(fsMock)
+    );
 
     expect(res).toMatchObject([
       {
@@ -136,11 +111,6 @@ describe('modules/manager/gradle/extract', () => {
           },
         ],
       },
-      {
-        datasource: 'maven',
-        deps: [],
-        packageFile: 'settings.gradle',
-      },
       { packageFile: 'build.gradle', deps: [] },
     ]);
   });
@@ -151,11 +121,8 @@ describe('modules/manager/gradle/extract', () => {
       'build.gradle': 'foo = "1.0.1"',
       'aaa/gradle.properties': 'bar = "2.0.0"',
       'aaa/build.gradle': 'bar = "2.0.1"',
-      'aaa/bbb/build.gradle': ['foo:foo:$foo', 'bar:bar:$bar']
-        .map((x) => `"${x}"`)
-        .join('\n'),
+      'aaa/bbb/build.gradle': '"foo:foo:$foo"; "bar:bar:$bar"',
     };
-
     mockFs(fsMock);
 
     const res = await extractAllPackageFiles(
@@ -178,446 +145,516 @@ describe('modules/manager/gradle/extract', () => {
     ]);
   });
 
-  it('deduplicates registry urls', async () => {
-    const fsMock = {
-      'build.gradle': [
-        'repositories { maven { url "https://repo.maven.apache.org/maven2" } }',
-        'repositories { maven { url "https://repo.maven.apache.org/maven2" } }',
-        'repositories { maven { url "https://example.com" } }',
-        'repositories { maven { url "https://example.com" } }',
-        'id "foo.bar" version "1.2.3"',
-        '"foo:bar:1.2.3"',
-      ].join(';\n'),
-    };
-
-    mockFs(fsMock);
-
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-
-    expect(res).toMatchObject([
-      {
-        packageFile: 'build.gradle',
-        deps: [
-          {
-            depType: 'plugin',
-            registryUrls: [
-              'https://repo.maven.apache.org/maven2',
-              'https://example.com',
-              'https://plugins.gradle.org/m2/',
-            ],
-          },
-          {
-            registryUrls: [
-              'https://repo.maven.apache.org/maven2',
-              'https://example.com',
-            ],
-          },
-        ],
-      },
-    ]);
-  });
+  it('filters duplicate dependency findings', async () => {
+    const buildFile = codeBlock`
+      apply from: 'test.gradle'
 
-  it('interpolates repository URLs', async () => {
-    const buildFile = `
       repositories {
-          mavenCentral()
-          maven {
-              url = "\${repositoryBaseURL}/repository-build"
-          }
-          maven {
-              name = "baz"
-              url = "\${repositoryBaseURL}/\${name}"
-          }
+        mavenCentral()
       }
 
       dependencies {
-          implementation "com.google.protobuf:protobuf-java:2.17.0"
+        implementation "io.jsonwebtoken:jjwt-api:$\{jjwtVersion}"
+        runtimeOnly "io.jsonwebtoken:jjwt-impl:$\{jjwtVersion}"
       }
     `;
 
-    mockFs({
-      'build.gradle': buildFile,
-      'gradle.properties': 'repositoryBaseURL: https\\://dummy.org/whatever',
-    });
-
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-      'gradle.properties',
-    ]);
-
-    expect(res).toMatchObject([
-      {
-        packageFile: 'gradle.properties',
-        datasource: 'maven',
-        deps: [],
-      },
-      {
-        packageFile: 'build.gradle',
-        datasource: 'maven',
-        deps: [
-          {
-            depName: 'com.google.protobuf:protobuf-java',
-            currentValue: '2.17.0',
-            managerData: {
-              fileReplacePosition: 335,
-              packageFile: 'build.gradle',
-            },
-            fileReplacePosition: 335,
-            registryUrls: [
-              'https://repo.maven.apache.org/maven2',
-              'https://dummy.org/whatever/repository-build',
-              'https://dummy.org/whatever/baz',
-            ],
-          },
-        ],
-      },
-    ]);
-  });
+    const testFile = codeBlock`
+      ext.jjwtVersion = '0.11.2'
 
-  it('works with dependency catalogs', async () => {
-    const tomlFile = Fixtures.get('1/libs.versions.toml');
+      ext {
+        jjwtApi = "io.jsonwebtoken:jjwt-api:$jjwtVersion"
+      }
+    `;
     const fsMock = {
-      'gradle/libs.versions.toml': tomlFile,
+      'build.gradle': buildFile,
+      'test.gradle': testFile,
     };
     mockFs(fsMock);
+
     const res = await extractAllPackageFiles(
       {} as ExtractConfig,
       Object.keys(fsMock)
     );
+
     expect(res).toMatchObject([
       {
-        packageFile: 'gradle/libs.versions.toml',
+        packageFile: 'test.gradle',
         deps: [
-          {
-            depName: 'io.gitlab.arturbosch.detekt:detekt-formatting',
-            groupName: 'detekt',
-            currentValue: '1.17.0',
-            managerData: {
-              fileReplacePosition: 21,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'io.kotest:kotest-assertions-core-jvm',
-            groupName: 'kotest',
-            currentValue: '4.6.0',
-            managerData: {
-              fileReplacePosition: 51,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'io.kotest:kotest-runner-junit5',
-            groupName: 'kotest',
-            currentValue: '4.6.0',
-            managerData: {
-              fileReplacePosition: 51,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'org.mockito:mockito-core',
-            groupName: 'org.mockito',
-            currentValue: '3.10.0',
-            managerData: {
-              fileReplacePosition: 474,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'com.github.siom79.japicmp:japicmp',
-            groupName: 'com.github.siom79.japicmp',
-            currentValue: '0.15.+',
-            managerData: {
-              fileReplacePosition: 561,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'guava',
-            skipReason: 'multiple-constraint-dep',
-            managerData: {
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'gson',
-            skipReason: 'unsupported-version',
-            managerData: {
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'io.gitlab.arturbosch.detekt',
-            depType: 'plugin',
-            currentValue: '1.17.0',
-            packageName:
-              'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin',
-            managerData: {
-              fileReplacePosition: 21,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-          },
-          {
-            depName: 'org.danilopianini.publish-on-central',
-            depType: 'plugin',
-            currentValue: '0.5.0',
-            packageName:
-              'org.danilopianini.publish-on-central:org.danilopianini.publish-on-central.gradle.plugin',
-            managerData: {
-              fileReplacePosition: 82,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-          },
-          {
-            depName: 'org.ajoberstar.grgit',
-            depType: 'plugin',
-            commitMessageTopic: 'plugin grgit',
-            packageName:
-              'org.ajoberstar.grgit:org.ajoberstar.grgit.gradle.plugin',
-            managerData: {
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-            skipReason: 'unknown-version',
-          },
+          { depName: 'io.jsonwebtoken:jjwt-api' },
+          { depName: 'io.jsonwebtoken:jjwt-impl' },
         ],
       },
+      { packageFile: 'build.gradle' },
     ]);
   });
 
-  it("can run Javier's example", async () => {
-    const tomlFile = Fixtures.get('2/libs.versions.toml');
+  it('ensures depType is assigned', async () => {
     const fsMock = {
-      'gradle/libs.versions.toml': tomlFile,
+      'build.gradle':
+        "id 'org.sonarqube' version '3.1.1'\n\"io.jsonwebtoken:jjwt-api:0.11.2\"",
+      'buildSrc/build.gradle': '"com.google.protobuf:protobuf-java:3.18.2"',
     };
     mockFs(fsMock);
+
     const res = await extractAllPackageFiles(
       {} as ExtractConfig,
       Object.keys(fsMock)
     );
+
     expect(res).toMatchObject([
       {
-        packageFile: 'gradle/libs.versions.toml',
+        packageFile: 'build.gradle',
         deps: [
-          {
-            depName: 'com.squareup.okhttp3:okhttp',
-            groupName: 'com.squareup.okhttp3',
-            currentValue: '4.9.0',
-            managerData: {
-              fileReplacePosition: 99,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'com.squareup.okio:okio',
-            groupName: 'com.squareup.okio',
-            currentValue: '2.8.0',
-            managerData: {
-              fileReplacePosition: 161,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'com.squareup.picasso:picasso',
-            groupName: 'com.squareup.picasso',
-            currentValue: '2.5.1',
-            managerData: {
-              fileReplacePosition: 243,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'com.squareup.retrofit2:retrofit',
-            groupName: 'retrofit',
-            currentValue: '2.8.2',
-            managerData: {
-              fileReplacePosition: 41,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-          },
-          {
-            depName: 'google-firebase-analytics',
-            managerData: {
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            skipReason: 'no-version',
-          },
-          {
-            depName: 'google-firebase-crashlytics',
-            managerData: {
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            skipReason: 'no-version',
-          },
-          {
-            depName: 'google-firebase-messaging',
-            managerData: {
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            skipReason: 'no-version',
-          },
-          {
-            depName: 'org.jetbrains.kotlin.jvm',
-            depType: 'plugin',
-            currentValue: '1.5.21',
-            commitMessageTopic: 'plugin kotlinJvm',
-            packageName:
-              'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin',
-            managerData: {
-              fileReplacePosition: 661,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-          },
-          {
-            depName: 'org.jetbrains.kotlin.plugin.serialization',
-            depType: 'plugin',
-            currentValue: '1.5.21',
-            packageName:
-              'org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin',
-            managerData: {
-              fileReplacePosition: 21,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-          },
-          {
-            depName: 'org.danilopianini.multi-jvm-test-plugin',
-            depType: 'plugin',
-            currentValue: '0.3.0',
-            commitMessageTopic: 'plugin multiJvm',
-            packageName:
-              'org.danilopianini.multi-jvm-test-plugin:org.danilopianini.multi-jvm-test-plugin.gradle.plugin',
-            managerData: {
-              fileReplacePosition: 822,
-              packageFile: 'gradle/libs.versions.toml',
-            },
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-          },
+          { depName: 'org.sonarqube', depType: 'plugin' },
+          { depName: 'io.jsonwebtoken:jjwt-api', depType: 'dependencies' },
         ],
       },
+      {
+        packageFile: 'buildSrc/build.gradle',
+        deps: [{ depType: 'devDependencies' }],
+      },
     ]);
   });
 
-  it('ignores an empty TOML', async () => {
-    const tomlFile = '';
-    const fsMock = {
-      'gradle/libs.versions.toml': tomlFile,
-    };
-    mockFs(fsMock);
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-    expect(res).toBeNull();
+  describe('registry URLs', () => {
+    it('deduplicates registry urls', async () => {
+      const fsMock = {
+        'build.gradle': codeBlock`
+          repositories { maven { url "https://repo.maven.apache.org/maven2" } }
+          repositories { maven { url "https://repo.maven.apache.org/maven2" } }
+          repositories { maven { url "https://example.com" } }
+          repositories { maven { url "https://example.com" } }
+          plugins { id "foo.bar" version "1.2.3" }
+          dependencies { classpath "foo:bar:1.2.3" }
+        `,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      expect(res).toMatchObject([
+        {
+          packageFile: 'build.gradle',
+          deps: [
+            {
+              depType: 'plugin',
+              registryUrls: [
+                'https://repo.maven.apache.org/maven2',
+                'https://example.com',
+                'https://plugins.gradle.org/m2/',
+              ],
+            },
+            {
+              registryUrls: [
+                'https://repo.maven.apache.org/maven2',
+                'https://example.com',
+              ],
+            },
+          ],
+        },
+      ]);
+    });
+
+    it('interpolates registry URLs', async () => {
+      const buildFile = codeBlock`
+        repositories {
+            mavenCentral()
+            maven {
+                url = "\${repositoryBaseURL}/repository-build"
+            }
+            maven {
+                name = "baz"
+                url = "\${repositoryBaseURL}/\${name}"
+            }
+        }
+
+        dependencies {
+            implementation "com.google.protobuf:protobuf-java:2.17.0"
+        }
+      `;
+
+      const fsMock = {
+        'build.gradle': buildFile,
+        'gradle.properties': 'repositoryBaseURL: https\\://dummy.org/whatever',
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      expect(res).toMatchObject([
+        {
+          packageFile: 'gradle.properties',
+          deps: [],
+        },
+        {
+          packageFile: 'build.gradle',
+          deps: [
+            {
+              depName: 'com.google.protobuf:protobuf-java',
+              currentValue: '2.17.0',
+              managerData: {
+                fileReplacePosition: 262,
+                packageFile: 'build.gradle',
+              },
+              fileReplacePosition: 262,
+              registryUrls: [
+                'https://repo.maven.apache.org/maven2',
+                'https://dummy.org/whatever/repository-build',
+                'https://dummy.org/whatever/baz',
+              ],
+            },
+          ],
+        },
+      ]);
+    });
   });
 
-  it('deletes commit message for plugins with version reference', async () => {
-    const tomlFile = `
-    [versions]
-    detekt = "1.18.1"
+  describe('version catalogs', () => {
+    it('works with dependency catalogs', async () => {
+      const fsMock = {
+        'gradle/libs.versions.toml': Fixtures.get('1/libs.versions.toml'),
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+      expect(res).toMatchObject([
+        {
+          packageFile: 'gradle/libs.versions.toml',
+          deps: [
+            {
+              depName: 'io.gitlab.arturbosch.detekt:detekt-formatting',
+              groupName: 'detekt',
+              currentValue: '1.17.0',
+              managerData: {
+                fileReplacePosition: 21,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'io.kotest:kotest-assertions-core-jvm',
+              groupName: 'kotest',
+              currentValue: '4.6.0',
+              managerData: {
+                fileReplacePosition: 51,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'io.kotest:kotest-runner-junit5',
+              groupName: 'kotest',
+              currentValue: '4.6.0',
+              managerData: {
+                fileReplacePosition: 51,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'org.mockito:mockito-core',
+              groupName: 'org.mockito',
+              currentValue: '3.10.0',
+              managerData: {
+                fileReplacePosition: 474,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'com.github.siom79.japicmp:japicmp',
+              groupName: 'com.github.siom79.japicmp',
+              currentValue: '0.15.+',
+              managerData: {
+                fileReplacePosition: 561,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'guava',
+              skipReason: 'multiple-constraint-dep',
+              managerData: {
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'gson',
+              skipReason: 'unsupported-version',
+              managerData: {
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'io.gitlab.arturbosch.detekt',
+              depType: 'plugin',
+              currentValue: '1.17.0',
+              packageName:
+                'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin',
+              managerData: {
+                fileReplacePosition: 21,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+            },
+            {
+              depName: 'org.danilopianini.publish-on-central',
+              depType: 'plugin',
+              currentValue: '0.5.0',
+              packageName:
+                'org.danilopianini.publish-on-central:org.danilopianini.publish-on-central.gradle.plugin',
+              managerData: {
+                fileReplacePosition: 82,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+            },
+            {
+              depName: 'org.ajoberstar.grgit',
+              depType: 'plugin',
+              commitMessageTopic: 'plugin grgit',
+              packageName:
+                'org.ajoberstar.grgit:org.ajoberstar.grgit.gradle.plugin',
+              managerData: {
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+              skipReason: 'unknown-version',
+            },
+          ],
+        },
+      ]);
+    });
 
-    [plugins]
-    detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
+    it('supports versions declared as single string', async () => {
+      const fsMock = {
+        'gradle/libs.versions.toml': Fixtures.get('2/libs.versions.toml'),
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      expect(res).toMatchObject([
+        {
+          packageFile: 'gradle/libs.versions.toml',
+          deps: [
+            {
+              depName: 'com.squareup.okhttp3:okhttp',
+              groupName: 'com.squareup.okhttp3',
+              currentValue: '4.9.0',
+              managerData: {
+                fileReplacePosition: 99,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'com.squareup.okio:okio',
+              groupName: 'com.squareup.okio',
+              currentValue: '2.8.0',
+              managerData: {
+                fileReplacePosition: 161,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'com.squareup.picasso:picasso',
+              groupName: 'com.squareup.picasso',
+              currentValue: '2.5.1',
+              managerData: {
+                fileReplacePosition: 243,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'com.squareup.retrofit2:retrofit',
+              groupName: 'retrofit',
+              currentValue: '2.8.2',
+              managerData: {
+                fileReplacePosition: 41,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+            },
+            {
+              depName: 'google-firebase-analytics',
+              managerData: {
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              skipReason: 'no-version',
+            },
+            {
+              depName: 'google-firebase-crashlytics',
+              managerData: {
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              skipReason: 'no-version',
+            },
+            {
+              depName: 'google-firebase-messaging',
+              managerData: {
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              skipReason: 'no-version',
+            },
+            {
+              depName: 'org.jetbrains.kotlin.jvm',
+              depType: 'plugin',
+              currentValue: '1.5.21',
+              commitMessageTopic: 'plugin kotlinJvm',
+              packageName:
+                'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin',
+              managerData: {
+                fileReplacePosition: 661,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+            },
+            {
+              depName: 'org.jetbrains.kotlin.plugin.serialization',
+              depType: 'plugin',
+              currentValue: '1.5.21',
+              packageName:
+                'org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin',
+              managerData: {
+                fileReplacePosition: 21,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+            },
+            {
+              depName: 'org.danilopianini.multi-jvm-test-plugin',
+              depType: 'plugin',
+              currentValue: '0.3.0',
+              commitMessageTopic: 'plugin multiJvm',
+              packageName:
+                'org.danilopianini.multi-jvm-test-plugin:org.danilopianini.multi-jvm-test-plugin.gradle.plugin',
+              managerData: {
+                fileReplacePosition: 822,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+            },
+          ],
+        },
+      ]);
+    });
 
-    [libraries]
-    detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
-    `;
-    const fsMock = {
-      'gradle/libs.versions.toml': tomlFile,
-    };
-    mockFs(fsMock);
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-    expect(res).toMatchObject([
-      {
-        packageFile: 'gradle/libs.versions.toml',
-        deps: [
-          {
-            depName: 'io.gitlab.arturbosch.detekt:detekt-formatting',
-            groupName: 'detekt',
-            currentValue: '1.18.1',
-            managerData: {
-              fileReplacePosition: 30,
-              packageFile: 'gradle/libs.versions.toml',
+    it('ignores empty TOML file', async () => {
+      const fsMock = {
+        'gradle/libs.versions.toml': '',
+      };
+      mockFs(fsMock);
+
+      expect(
+        await extractAllPackageFiles({} as ExtractConfig, Object.keys(fsMock))
+      ).toBeNull();
+    });
+
+    it('deletes commit message for plugins with version reference', async () => {
+      const fsMock = {
+        'gradle/libs.versions.toml': stripIndent`
+        [versions]
+        detekt = "1.18.1"
+
+        [plugins]
+        detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
+
+        [libraries]
+        detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
+      `,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+      expect(res).toMatchObject([
+        {
+          packageFile: 'gradle/libs.versions.toml',
+          deps: [
+            {
+              depName: 'io.gitlab.arturbosch.detekt:detekt-formatting',
+              groupName: 'detekt',
+              currentValue: '1.18.1',
+              managerData: {
+                fileReplacePosition: 21,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              fileReplacePosition: 21,
             },
-            fileReplacePosition: 30,
-            registryUrls: [],
-          },
-          {
-            depType: 'plugin',
-            depName: 'io.gitlab.arturbosch.detekt',
-            packageName:
-              'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin',
-            registryUrls: ['https://plugins.gradle.org/m2/'],
-            currentValue: '1.18.1',
-            managerData: {
-              fileReplacePosition: 30,
-              packageFile: 'gradle/libs.versions.toml',
+            {
+              depType: 'plugin',
+              depName: 'io.gitlab.arturbosch.detekt',
+              packageName:
+                'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin',
+              registryUrls: ['https://plugins.gradle.org/m2/'],
+              currentValue: '1.18.1',
+              managerData: {
+                fileReplacePosition: 21,
+                packageFile: 'gradle/libs.versions.toml',
+              },
+              groupName: 'detekt',
+              fileReplacePosition: 21,
             },
-            groupName: 'detekt',
-            fileReplacePosition: 30,
-          },
-        ],
-      },
-    ]);
-  });
+          ],
+        },
+      ]);
+    });
 
-  it('should change the dependency version not the comment version', async () => {
-    const tomlFile = Fixtures.get('3/libs.versions.toml');
-    const fsMock = {
-      'gradle/libs.versions.toml': tomlFile,
-    };
-    mockFs(fsMock);
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-    expect(res).toMatchObject([
-      {
-        packageFile: 'gradle/libs.versions.toml',
-        datasource: 'maven',
-        deps: [
-          {
-            depName: 'junit:junit',
-            groupName: 'junit',
-            currentValue: '1.4.9',
-            managerData: {
+    it('changes the dependency version, not the comment version', async () => {
+      const fsMock = {
+        'gradle/libs.versions.toml': Fixtures.get('3/libs.versions.toml'),
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+      expect(res).toMatchObject([
+        {
+          packageFile: 'gradle/libs.versions.toml',
+          deps: [
+            {
+              depName: 'junit:junit',
+              groupName: 'junit',
+              currentValue: '1.4.9',
+              managerData: {
+                fileReplacePosition: 124,
+                packageFile: 'gradle/libs.versions.toml',
+              },
               fileReplacePosition: 124,
-              packageFile: 'gradle/libs.versions.toml',
             },
-            fileReplacePosition: 124,
-            registryUrls: [],
-          },
-          {
-            depName: 'mocha-junit:mocha-junit',
-            groupName: 'mocha-junit-reporter',
-            currentValue: '2.0.2',
-            managerData: {
+            {
+              depName: 'mocha-junit:mocha-junit',
+              groupName: 'mocha-junit-reporter',
+              currentValue: '2.0.2',
+              managerData: {
+                fileReplacePosition: 82,
+                packageFile: 'gradle/libs.versions.toml',
+              },
               fileReplacePosition: 82,
-              packageFile: 'gradle/libs.versions.toml',
             },
-            fileReplacePosition: 82,
-            registryUrls: [],
-          },
-        ],
-      },
-    ]);
+          ],
+        },
+      ]);
+    });
   });
 
-  it('loads further scripts using apply from statements', async () => {
-    const buildFile = `
-      buildscript {
+  describe('apply from', () => {
+    it('loads further scripts using apply from statements', async () => {
+      const buildFile = codeBlock`
+        buildscript {
           repositories {
-              mavenCentral()
+            mavenCentral()
           }
 
           apply from: "\${someDir}/libs1.gradle"
@@ -627,408 +664,322 @@ describe('modules/manager/gradle/extract', () => {
           apply from: file("gradle/non-existing.gradle")
 
           dependencies {
-              classpath "com.google.protobuf:protobuf-java:\${protoBufVersion}"
-              classpath "com.google.guava:guava:\${guavaVersion}"
-              classpath "io.jsonwebtoken:jjwt-api:0.11.2"
+            classpath "com.google.protobuf:protobuf-java:\${protoBufVersion}"
+            classpath "com.google.guava:guava:\${guavaVersion}"
+            classpath "io.jsonwebtoken:jjwt-api:0.11.2"
 
-              classpath "org.junit.jupiter:junit-jupiter-api:\${junitVersion}"
-              classpath "org.junit.jupiter:junit-jupiter-engine:\${junitVersion}"
-              classpath "org.slf4j:slf4j-api:\${slf4jVersion}"
+            classpath "org.junit.jupiter:junit-jupiter-api:\${junitVersion}"
+            classpath "org.junit.jupiter:junit-jupiter-engine:\${junitVersion}"
+            classpath "org.slf4j:slf4j-api:\${slf4jVersion}"
           }
-      }
-    `;
-
-    mockFs({
-      'gradleX/libs1.gradle': "ext.junitVersion = '5.5.2'",
-      'gradle/libs2.gradle': "ext.protoBufVersion = '3.18.2'",
-      'gradle/libs3.gradle': "ext.guavaVersion = '30.1-jre'",
-      'gradleX/gradleX/libs4.gradle': "ext.slf4jVersion = '1.7.30'",
-      'build.gradle': buildFile,
-      'gradle.properties': 'someDir=gradleX',
+        }
+      `;
+
+      const fsMock = {
+        'gradleX/libs1.gradle': "ext.junitVersion = '5.5.2'",
+        'gradle/libs2.gradle': "ext.protoBufVersion = '3.18.2'",
+        'gradle/libs3.gradle': "ext.guavaVersion = '30.1-jre'",
+        'gradleX/gradleX/libs4.gradle': "ext.slf4jVersion = '1.7.30'",
+        'build.gradle': buildFile,
+        'gradle.properties': 'someDir=gradleX',
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      expect(res).toMatchObject([
+        { packageFile: 'gradle.properties' },
+        {
+          packageFile: 'build.gradle',
+          deps: [{ depName: 'io.jsonwebtoken:jjwt-api' }],
+        },
+        {
+          packageFile: 'gradle/libs2.gradle',
+          deps: [
+            {
+              depName: 'com.google.protobuf:protobuf-java',
+              currentValue: '3.18.2',
+              managerData: { packageFile: 'gradle/libs2.gradle' },
+            },
+          ],
+        },
+        {
+          packageFile: 'gradle/libs3.gradle',
+          deps: [
+            {
+              depName: 'com.google.guava:guava',
+              currentValue: '30.1-jre',
+              managerData: { packageFile: 'gradle/libs3.gradle' },
+            },
+          ],
+        },
+        {
+          packageFile: 'gradleX/libs1.gradle',
+          deps: [
+            {
+              depName: 'org.junit.jupiter:junit-jupiter-api',
+              currentValue: '5.5.2',
+              managerData: { packageFile: 'gradleX/libs1.gradle' },
+            },
+            {
+              depName: 'org.junit.jupiter:junit-jupiter-engine',
+              currentValue: '5.5.2',
+              managerData: { packageFile: 'gradleX/libs1.gradle' },
+            },
+          ],
+        },
+        {
+          packageFile: 'gradleX/gradleX/libs4.gradle',
+          deps: [
+            {
+              depName: 'org.slf4j:slf4j-api',
+              currentValue: '1.7.30',
+              managerData: { packageFile: 'gradleX/gradleX/libs4.gradle' },
+            },
+          ],
+        },
+      ]);
     });
 
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'gradleX/libs1.gradle',
-      'gradle/libs2.gradle',
-      'gradle/libs3.gradle',
-      'gradleX/gradleX/libs4.gradle',
-      'build.gradle',
-      'gradle.properties',
-    ]);
-
-    expect(res).toMatchObject([
-      { packageFile: 'gradle.properties' },
-      {
-        packageFile: 'build.gradle',
-        deps: [{ depName: 'io.jsonwebtoken:jjwt-api' }],
-      },
-      {
-        packageFile: 'gradle/libs2.gradle',
-        deps: [
-          {
-            depName: 'com.google.protobuf:protobuf-java',
-            currentValue: '3.18.2',
-            managerData: { packageFile: 'gradle/libs2.gradle' },
-          },
-        ],
-      },
-      {
-        packageFile: 'gradle/libs3.gradle',
-        deps: [
-          {
-            depName: 'com.google.guava:guava',
-            currentValue: '30.1-jre',
-            managerData: { packageFile: 'gradle/libs3.gradle' },
-          },
-        ],
-      },
-      {
-        packageFile: 'gradleX/libs1.gradle',
-        deps: [
-          {
-            depName: 'org.junit.jupiter:junit-jupiter-api',
-            currentValue: '5.5.2',
-            managerData: { packageFile: 'gradleX/libs1.gradle' },
-          },
-          {
-            depName: 'org.junit.jupiter:junit-jupiter-engine',
-            currentValue: '5.5.2',
-            managerData: { packageFile: 'gradleX/libs1.gradle' },
-          },
-        ],
-      },
-      {
-        packageFile: 'gradleX/gradleX/libs4.gradle',
-        deps: [
-          {
-            depName: 'org.slf4j:slf4j-api',
-            currentValue: '1.7.30',
-            managerData: { packageFile: 'gradleX/gradleX/libs4.gradle' },
-          },
-        ],
-      },
-    ]);
-  });
-
-  it('apply from works with files in sub-directories', async () => {
-    const buildFile = `
-      buildscript {
-          repositories {
-              mavenCentral()
-          }
-
-          apply from: "gradle/libs4.gradle"
-
-          dependencies {
-              classpath "com.google.protobuf:protobuf-java:\${protoBufVersion}"
-          }
-      }
-    `;
-
-    mockFs({
-      'somesubdir/gradle/libs4.gradle': "ext.protoBufVersion = '3.18.2'",
-      'somesubdir/build.gradle': buildFile,
+    it('works with files in sub-directories', async () => {
+      const buildFile = `
+        buildscript {
+            repositories {
+                mavenCentral()
+            }
+
+            apply from: "gradle/libs4.gradle"
+
+            dependencies {
+                classpath "com.google.protobuf:protobuf-java:\${protoBufVersion}"
+            }
+        }
+      `;
+
+      const fsMock = {
+        'somesubdir/gradle/libs4.gradle': "ext.protoBufVersion = '3.18.2'",
+        'somesubdir/build.gradle': buildFile,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      expect(res).toMatchObject([
+        { packageFile: 'somesubdir/build.gradle' },
+        {
+          packageFile: 'somesubdir/gradle/libs4.gradle',
+          deps: [{ depName: 'com.google.protobuf:protobuf-java' }],
+        },
+      ]);
     });
 
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'somesubdir/gradle/libs4.gradle',
-      'somesubdir/build.gradle',
-    ]);
-
-    expect(res).toMatchObject([
-      { packageFile: 'somesubdir/build.gradle' },
-      {
-        packageFile: 'somesubdir/gradle/libs4.gradle',
-        deps: [{ depName: 'com.google.protobuf:protobuf-java' }],
-      },
-    ]);
-  });
+    it('prevents recursive apply from calls', async () => {
+      const fsMock = {
+        'build.gradle': "apply from: 'test.gradle'",
+        'test.gradle': "apply from: 'build.gradle'",
+      };
+      mockFs(fsMock);
 
-  it('prevents recursive apply from calls', async () => {
-    mockFs({
-      'build.gradle': "apply from: 'test.gradle'",
-      'test.gradle': "apply from: 'build.gradle'",
+      expect(
+        await extractAllPackageFiles({} as ExtractConfig, Object.keys(fsMock))
+      ).toBeNull();
     });
 
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-      'test.gradle',
-    ]);
-
-    expect(res).toBeNull();
-  });
+    it('prevents inclusion of non-Gradle files', async () => {
+      const fsMock = {
+        'build.gradle': "apply from: '../../test.non-gradle'",
+      };
+      mockFs(fsMock);
 
-  it('prevents inclusion of non-Gradle files', async () => {
-    mockFs({
-      'build.gradle': "apply from: '../../test.non-gradle'",
+      expect(
+        await extractAllPackageFiles({} as ExtractConfig, Object.keys(fsMock))
+      ).toBeNull();
     });
-
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-    ]);
-
-    expect(res).toBeNull();
-  });
-
-  it('filters duplicate dependency findings', async () => {
-    const buildFile = `
-      apply from: 'test.gradle'
-
-      repositories {
-          mavenCentral()
-      }
-
-      dependencies {
-        implementation "io.jsonwebtoken:jjwt-api:$\{jjwtVersion}"
-        runtimeOnly "io.jsonwebtoken:jjwt-impl:$\{jjwtVersion}"
-      }
-    `;
-
-    const testFile = `
-      ext.jjwtVersion = '0.11.2'
-
-      ext {
-          jjwtApi = "io.jsonwebtoken:jjwt-api:$jjwtVersion"
-      }
-    `;
-
-    mockFs({
-      'build.gradle': buildFile,
-      'test.gradle': testFile,
-    });
-
-    const res = await extractAllPackageFiles({} as ExtractConfig, [
-      'build.gradle',
-      'test.gradle',
-    ]);
-
-    expect(res).toMatchObject([
-      {
-        packageFile: 'test.gradle',
-        deps: [
-          { depName: 'io.jsonwebtoken:jjwt-api' },
-          { depName: 'io.jsonwebtoken:jjwt-impl' },
-        ],
-      },
-      { packageFile: 'build.gradle' },
-    ]);
   });
 
-  it('ensures depType is assigned', async () => {
-    const fsMock = {
-      'build.gradle':
-        "id 'org.sonarqube' version '3.1.1'\n\"io.jsonwebtoken:jjwt-api:0.11.2\"",
-      'buildSrc/build.gradle': '"com.google.protobuf:protobuf-java:3.18.2"',
-    };
-
-    mockFs(fsMock);
-
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-
-    expect(res).toMatchObject([
-      {
-        packageFile: 'build.gradle',
-        deps: [
-          { depName: 'org.sonarqube', depType: 'plugin' },
-          { depName: 'io.jsonwebtoken:jjwt-api', depType: 'dependencies' },
-        ],
-      },
-      {
-        packageFile: 'buildSrc/build.gradle',
-        deps: [{ depType: 'devDependencies' }],
-      },
-    ]);
-  });
-
-  // Tests for gradle-consistent-version plugin
-  it('gradle-consistent-versions parse versions files', async () => {
-    const fsMock = {
-      'versions.props': `org.apache.lucene:* = 1.2.3`,
-      'versions.lock': stripIndent`
-        # Run ./gradlew --write-locks to regenerate this file
-        org.apache.lucene:lucene-core:1.2.3 (10 constraints: 95be0c15)
-        org.apache.lucene:lucene-codecs:1.2.3 (5 constraints: 1231231)
-      `,
-    };
-
-    mockFs(fsMock);
-
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-
-    expect(res).toMatchObject([
-      {
-        packageFile: 'versions.lock',
-      },
-      {
-        packageFile: 'versions.props',
-        deps: [
-          {
-            depName: 'org.apache.lucene:lucene-core',
-            depType: 'dependencies',
-            fileReplacePosition: 22,
-            groupName: 'org.apache.lucene:*',
-            lockedVersion: '1.2.3',
-            managerData: {
+  describe('gradle-consistent-versions plugin', () => {
+    it('parses versions files', async () => {
+      const fsMock = {
+        'versions.props': `org.apache.lucene:* = 1.2.3`,
+        'versions.lock': stripIndent`
+          # Run ./gradlew --write-locks to regenerate this file
+          org.apache.lucene:lucene-core:1.2.3 (10 constraints: 95be0c15)
+          org.apache.lucene:lucene-codecs:1.2.3 (5 constraints: 1231231)
+        `,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      expect(res).toMatchObject([
+        {
+          packageFile: 'versions.lock',
+        },
+        {
+          packageFile: 'versions.props',
+          deps: [
+            {
+              depName: 'org.apache.lucene:lucene-core',
+              depType: 'dependencies',
               fileReplacePosition: 22,
-              packageFile: 'versions.props',
-            },
-            registryUrls: [],
-          },
-          {
-            depName: 'org.apache.lucene:lucene-codecs',
-            depType: 'dependencies',
-            fileReplacePosition: 22,
-            groupName: 'org.apache.lucene:*',
-            lockedVersion: '1.2.3',
-            managerData: {
+              groupName: 'org.apache.lucene:*',
+              lockedVersion: '1.2.3',
+              managerData: {
+                fileReplacePosition: 22,
+                packageFile: 'versions.props',
+              },
+            },
+            {
+              depName: 'org.apache.lucene:lucene-codecs',
+              depType: 'dependencies',
               fileReplacePosition: 22,
-              packageFile: 'versions.props',
-            },
-            registryUrls: [],
-          },
-        ],
-      },
-    ]);
-  });
-
-  it('gradle-consistent-versions plugin not used due to lockfile not a GCV lockfile', async () => {
-    const fsMock = {
-      'versions.props': `org.apache.lucene:* = 1.2.3`,
-      'versions.lock': stripIndent`
-        This is NOT a lock file
-      `,
-    };
-    mockFs(fsMock);
-
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-    expect(res).toBeNull();
-  });
-
-  it('gradle-consistent-versions plugin not used due to lockfile missing', async () => {
-    const fsMock = {
-      'build.gradle': '(this file contains) com.palantir.consistent-versions',
-      'versions.props': `org.apache.lucene:* = 1.2.3`,
-    };
-    mockFs(fsMock);
-
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
-    expect(res).toBeNull();
-  });
+              groupName: 'org.apache.lucene:*',
+              lockedVersion: '1.2.3',
+              managerData: {
+                fileReplacePosition: 22,
+                packageFile: 'versions.props',
+              },
+            },
+          ],
+        },
+      ]);
+    });
 
-  it('gradle-consistent-versions multi levels of glob', async () => {
-    const fsMock = {
-      'versions.props': stripIndent`
-        org.apache.* = 4
-        org.apache.lucene:* = 3
-        org.apache.lucene:a.* = 2
-        org.apache.lucene:a.b = 1
-      `,
-      'versions.lock': stripIndent`
-        # Run ./gradlew --write-locks to regenerate this file
-        org.apache.solr:x.y:1 (10 constraints: 95be0c15)
-        org.apache.lucene:a.b:1 (10 constraints: 95be0c15)
-        org.apache.lucene:a.c:1 (10 constraints: 95be0c15)
-        org.apache.lucene:a.d:1 (10 constraints: 95be0c15)
-        org.apache.lucene:d:1 (10 constraints: 95be0c15)
-        org.apache.lucene:e.f:1 (10 constraints: 95be0c15)
-      `,
-    };
-    mockFs(fsMock);
+    it('plugin not used due to lockfile not a GCV lockfile', async () => {
+      const fsMock = {
+        'versions.props': `org.apache.lucene:* = 1.2.3`,
+        'versions.lock': stripIndent`
+          This is NOT a lock file
+        `,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+      expect(res).toBeNull();
+    });
 
-    const res = await extractAllPackageFiles(
-      {} as ExtractConfig,
-      Object.keys(fsMock)
-    );
+    it('plugin not used due to lockfile missing', async () => {
+      const fsMock = {
+        'build.gradle': '(this file contains) com.palantir.consistent-versions',
+        'versions.props': `org.apache.lucene:* = 1.2.3`,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+      expect(res).toBeNull();
+    });
 
-    // Each lock dep is only present once, with highest prio for exact prop match, then globs from longest to shortest
-    expect(res).toMatchObject([
-      {
-        packageFile: 'versions.lock',
-        datasource: 'maven',
-        deps: [],
-      },
-      {
-        packageFile: 'versions.props',
-        datasource: 'maven',
-        deps: [
-          {
-            managerData: {
-              packageFile: 'versions.props',
+    it('supports multiple levels of glob', async () => {
+      const fsMock = {
+        'versions.props': stripIndent`
+          org.apache.* = 4
+          org.apache.lucene:* = 3
+          org.apache.lucene:a.* = 2
+          org.apache.lucene:a.b = 1
+        `,
+        'versions.lock': stripIndent`
+          # Run ./gradlew --write-locks to regenerate this file
+          org.apache.solr:x.y:1 (10 constraints: 95be0c15)
+          org.apache.lucene:a.b:1 (10 constraints: 95be0c15)
+          org.apache.lucene:a.c:1 (10 constraints: 95be0c15)
+          org.apache.lucene:a.d:1 (10 constraints: 95be0c15)
+          org.apache.lucene:d:1 (10 constraints: 95be0c15)
+          org.apache.lucene:e.f:1 (10 constraints: 95be0c15)
+        `,
+      };
+      mockFs(fsMock);
+
+      const res = await extractAllPackageFiles(
+        {} as ExtractConfig,
+        Object.keys(fsMock)
+      );
+
+      // Each lock dep is only present once, with highest prio for exact prop match, then globs from longest to shortest
+      expect(res).toMatchObject([
+        {
+          packageFile: 'versions.lock',
+          deps: [],
+        },
+        {
+          packageFile: 'versions.props',
+          deps: [
+            {
+              managerData: {
+                packageFile: 'versions.props',
+                fileReplacePosition: 91,
+              },
+              depName: 'org.apache.lucene:a.b',
+              currentValue: '1',
+              lockedVersion: '1',
               fileReplacePosition: 91,
-            },
-            depName: 'org.apache.lucene:a.b',
-            currentValue: '1',
-            lockedVersion: '1',
-            fileReplacePosition: 91,
-            registryUrls: [],
-            depType: 'dependencies',
-          },
-          {
-            managerData: {
-              packageFile: 'versions.props',
+              depType: 'dependencies',
+            },
+            {
+              managerData: {
+                packageFile: 'versions.props',
+                fileReplacePosition: 65,
+              },
+              depName: 'org.apache.lucene:a.c',
+              currentValue: '2',
+              lockedVersion: '1',
+              groupName: 'org.apache.lucene:a.*',
               fileReplacePosition: 65,
-            },
-            depName: 'org.apache.lucene:a.c',
-            currentValue: '2',
-            lockedVersion: '1',
-            groupName: 'org.apache.lucene:a.*',
-            fileReplacePosition: 65,
-            registryUrls: [],
-            depType: 'dependencies',
-          },
-          {
-            managerData: {
-              packageFile: 'versions.props',
+              depType: 'dependencies',
+            },
+            {
+              managerData: {
+                packageFile: 'versions.props',
+                fileReplacePosition: 65,
+              },
+              depName: 'org.apache.lucene:a.d',
+              currentValue: '2',
+              lockedVersion: '1',
+              groupName: 'org.apache.lucene:a.*',
               fileReplacePosition: 65,
-            },
-            depName: 'org.apache.lucene:a.d',
-            currentValue: '2',
-            lockedVersion: '1',
-            groupName: 'org.apache.lucene:a.*',
-            fileReplacePosition: 65,
-            registryUrls: [],
-            depType: 'dependencies',
-          },
-          {
-            managerData: {
-              packageFile: 'versions.props',
+              depType: 'dependencies',
+            },
+            {
+              managerData: {
+                packageFile: 'versions.props',
+                fileReplacePosition: 39,
+              },
+              depName: 'org.apache.lucene:d',
+              currentValue: '3',
+              lockedVersion: '1',
+              groupName: 'org.apache.lucene:*',
               fileReplacePosition: 39,
-            },
-            depName: 'org.apache.lucene:d',
-            currentValue: '3',
-            lockedVersion: '1',
-            groupName: 'org.apache.lucene:*',
-            fileReplacePosition: 39,
-            registryUrls: [],
-            depType: 'dependencies',
-          },
-          {
-            managerData: {
-              packageFile: 'versions.props',
+              depType: 'dependencies',
+            },
+            {
+              managerData: {
+                packageFile: 'versions.props',
+                fileReplacePosition: 39,
+              },
+              depName: 'org.apache.lucene:e.f',
+              currentValue: '3',
+              lockedVersion: '1',
+              groupName: 'org.apache.lucene:*',
               fileReplacePosition: 39,
+              depType: 'dependencies',
             },
-            depName: 'org.apache.lucene:e.f',
-            currentValue: '3',
-            lockedVersion: '1',
-            groupName: 'org.apache.lucene:*',
-            fileReplacePosition: 39,
-            registryUrls: [],
-            depType: 'dependencies',
-          },
-        ],
-      },
-    ]);
+          ],
+        },
+      ]);
+    });
   });
 });
diff --git a/lib/modules/manager/gradle/extract.ts b/lib/modules/manager/gradle/extract.ts
index 01bc483297..f3ff86968d 100644
--- a/lib/modules/manager/gradle/extract.ts
+++ b/lib/modules/manager/gradle/extract.ts
@@ -26,29 +26,14 @@ import {
 
 const datasource = MavenDatasource.id;
 
-// Enables reverse sorting in generateBranchConfig()
-//
-// Required for grouped dependencies to be upgraded
-// correctly in single branch.
-//
-// https://github.com/renovatebot/renovate/issues/8224
-function elevateFileReplacePositionField(
-  deps: PackageDependency<GradleManagerData>[]
-): PackageDependency<GradleManagerData>[] {
-  return deps.map((dep) => ({
-    ...dep,
-    fileReplacePosition: dep?.managerData?.fileReplacePosition,
-  }));
-}
-
 export async function extractAllPackageFiles(
   config: ExtractConfig,
   packageFiles: string[]
 ): Promise<PackageFile[] | null> {
   const extractedDeps: PackageDependency<GradleManagerData>[] = [];
-  const registry: VariableRegistry = {};
+  const varRegistry: VariableRegistry = {};
   const packageFilesByName: Record<string, PackageFile> = {};
-  const registryUrls: string[] = [];
+  const packageRegistries: string[] = [];
   const reorderedFiles = reorderFiles(packageFiles);
   const fileContents = await getFileContentMap(packageFiles, true);
 
@@ -62,11 +47,11 @@ export async function extractAllPackageFiles(
     try {
       // TODO #7154
       const content = fileContents[packageFile]!;
-      const dir = upath.dirname(toAbsolutePath(packageFile));
+      const packageFileDir = upath.dirname(toAbsolutePath(packageFile));
 
       const updateVars = (newVars: PackageVariables): void => {
-        const oldVars = registry[dir] || {};
-        registry[dir] = { ...oldVars, ...newVars };
+        const oldVars = varRegistry[packageFileDir] || {};
+        varRegistry[packageFileDir] = { ...oldVars, ...newVars };
       };
 
       if (isPropsFile(packageFile)) {
@@ -83,18 +68,21 @@ export async function extractAllPackageFiles(
         const updatesFromGcv = parseGcv(packageFile, fileContents);
         extractedDeps.push(...updatesFromGcv);
       } else if (isGradleScriptFile(packageFile)) {
-        const vars = getVars(registry, dir);
+        const vars = getVars(varRegistry, packageFileDir);
         const {
           deps,
           urls,
           vars: gradleVars,
         } = parseGradle(content, vars, packageFile, fileContents);
-        urls.forEach((url) => {
-          if (!registryUrls.includes(url)) {
-            registryUrls.push(url);
+        for (const url of urls) {
+          if (!packageRegistries.includes(url)) {
+            packageRegistries.push(url);
           }
-        });
-        registry[dir] = { ...registry[dir], ...gradleVars };
+        }
+        varRegistry[packageFileDir] = {
+          ...varRegistry[packageFileDir],
+          ...gradleVars,
+        };
         updateVars(gradleVars);
         extractedDeps.push(...deps);
       }
@@ -110,22 +98,24 @@ export async function extractAllPackageFiles(
     return null;
   }
 
-  elevateFileReplacePositionField(extractedDeps).forEach((dep) => {
+  for (const dep of extractedDeps) {
+    dep.fileReplacePosition = dep?.managerData?.fileReplacePosition; // #8224
+
     const key = dep.managerData?.packageFile;
     // istanbul ignore else
     if (key) {
-      let pkgFile = packageFilesByName[key];
+      let pkgFile: PackageFile = packageFilesByName[key];
       // istanbul ignore if: won't happen if "apply from" processes only initially known files
       if (!pkgFile) {
         pkgFile = {
           packageFile: key,
           datasource,
           deps: [],
-        } as PackageFile;
+        };
       }
 
       dep.registryUrls = [
-        ...new Set([...registryUrls, ...(dep.registryUrls ?? [])]),
+        ...new Set([...packageRegistries, ...(dep.registryUrls ?? [])]),
       ];
 
       if (!dep.depType) {
@@ -148,8 +138,7 @@ export async function extractAllPackageFiles(
     } else {
       logger.warn({ dep }, `Failed to process Gradle dependency`);
     }
-  });
+  }
 
-  const result = Object.values(packageFilesByName);
-  return result;
+  return Object.values(packageFilesByName);
 }
diff --git a/lib/modules/manager/gradle/parser.ts b/lib/modules/manager/gradle/parser.ts
index 5c2a486251..ec245fa0c0 100644
--- a/lib/modules/manager/gradle/parser.ts
+++ b/lib/modules/manager/gradle/parser.ts
@@ -49,7 +49,7 @@ export function parseGradle(
 
     globalVars: initVars,
     deps: [],
-    depRegistryUrls: [],
+    registryUrls: [],
 
     varTokens: [],
     tmpTokenStore: {},
@@ -59,7 +59,7 @@ export function parseGradle(
   if (parsedResult) {
     deps.push(...parsedResult.deps);
     vars = { ...vars, ...parsedResult.globalVars };
-    urls.push(...parsedResult.depRegistryUrls);
+    urls.push(...parsedResult.registryUrls);
   }
 
   return { deps, urls, vars };
diff --git a/lib/modules/manager/gradle/parser/common.spec.ts b/lib/modules/manager/gradle/parser/common.spec.ts
index 99579709c0..c1b6879406 100644
--- a/lib/modules/manager/gradle/parser/common.spec.ts
+++ b/lib/modules/manager/gradle/parser/common.spec.ts
@@ -23,7 +23,7 @@ describe('modules/manager/gradle/parser/common', () => {
 
       globalVars: {},
       deps: [],
-      depRegistryUrls: [],
+      registryUrls: [],
 
       varTokens: [],
       tmpTokenStore: {},
diff --git a/lib/modules/manager/gradle/parser/handlers.ts b/lib/modules/manager/gradle/parser/handlers.ts
index 2ab626254c..043a896ddc 100644
--- a/lib/modules/manager/gradle/parser/handlers.ts
+++ b/lib/modules/manager/gradle/parser/handlers.ts
@@ -248,7 +248,7 @@ export function handlePlugin(ctx: Ctx): Ctx {
 
 export function handlePredefinedRegistryUrl(ctx: Ctx): Ctx {
   const registryName = loadFromTokenMap(ctx, 'registryUrl')[0].value;
-  ctx.depRegistryUrls.push(
+  ctx.registryUrls.push(
     REGISTRY_URLS[registryName as keyof typeof REGISTRY_URLS]
   );
 
@@ -281,7 +281,7 @@ export function handleCustomRegistryUrl(ctx: Ctx): Ctx {
     try {
       const { host, protocol } = url.parse(registryUrl);
       if (host && protocol) {
-        ctx.depRegistryUrls.push(registryUrl);
+        ctx.registryUrls.push(registryUrl);
       }
     } catch (e) {
       // no-op
@@ -370,7 +370,7 @@ export function handleApplyFrom(ctx: Ctx): Ctx {
 
   ctx.deps.push(...matchResult.deps);
   ctx.globalVars = { ...ctx.globalVars, ...matchResult.vars };
-  ctx.depRegistryUrls.push(...matchResult.urls);
+  ctx.registryUrls.push(...matchResult.urls);
 
   return ctx;
 }
diff --git a/lib/modules/manager/gradle/types.ts b/lib/modules/manager/gradle/types.ts
index b455142ce8..d68c39cd30 100644
--- a/lib/modules/manager/gradle/types.ts
+++ b/lib/modules/manager/gradle/types.ts
@@ -74,7 +74,7 @@ export interface Ctx {
 
   globalVars: PackageVariables;
   deps: PackageDependency<GradleManagerData>[];
-  depRegistryUrls: string[];
+  registryUrls: string[];
 
   varTokens: lexer.Token[];
   tmpTokenStore: Record<string, lexer.Token[]>;
-- 
GitLab