From a1c51509770a734d06b8fb9b771fbc79ca4cea3c Mon Sep 17 00:00:00 2001
From: Julien Tanay <julien.tanay@doctolib.com>
Date: Fri, 15 Nov 2024 11:40:22 +0100
Subject: [PATCH] feat(bundler): add source variables support (#32337)

---
 lib/modules/manager/bundler/extract.spec.ts | 26 +++++++++++++
 lib/modules/manager/bundler/extract.ts      | 43 +++++++++++++++++----
 2 files changed, 62 insertions(+), 7 deletions(-)

diff --git a/lib/modules/manager/bundler/extract.spec.ts b/lib/modules/manager/bundler/extract.spec.ts
index cbee5d033f..54fd4b4cfe 100644
--- a/lib/modules/manager/bundler/extract.spec.ts
+++ b/lib/modules/manager/bundler/extract.spec.ts
@@ -1,4 +1,5 @@
 import is from '@sindresorhus/is';
+import { codeBlock } from 'common-tags';
 import { Fixtures } from '../../../../test/fixtures';
 import { fs } from '../../../../test/util';
 import { isValid } from '../../versioning/ruby';
@@ -141,4 +142,29 @@ describe('modules/manager/bundler/extract', () => {
       { depName: 'sfn_my_dep2', currentValue: '"~> 1"' },
     ]);
   });
+
+  it('parses source variable in Gemfile', async () => {
+    const sourceVariableGemfile = codeBlock`
+      source "https://rubygems.org"
+      ruby '~> 1.5.3'
+      foo = 'https://gems.foo.com'
+      bar = 'https://gems.bar.com'
+
+      source foo
+
+      source bar do
+        gem "some_internal_gem"
+      end
+    `;
+
+    fs.readLocalFile.mockResolvedValueOnce(sourceVariableGemfile);
+    const res = await extractPackageFile(sourceVariableGemfile, 'Gemfile');
+    expect(res?.deps).toHaveLength(2);
+    expect(res?.registryUrls).toHaveLength(2);
+    expect(res?.registryUrls?.[1]).toBe('https://gems.foo.com');
+    expect(res?.deps[1]).toMatchObject({
+      depName: 'some_internal_gem',
+      registryUrls: ['https://gems.bar.com'],
+    });
+  });
 });
diff --git a/lib/modules/manager/bundler/extract.ts b/lib/modules/manager/bundler/extract.ts
index 54995620a9..8087f2afad 100644
--- a/lib/modules/manager/bundler/extract.ts
+++ b/lib/modules/manager/bundler/extract.ts
@@ -78,6 +78,9 @@ export async function extractPackageFile(
     registryUrls: [],
     deps: [],
   };
+
+  const variables: Record<string, string> = {};
+
   const lines = content.split(newlineRegex);
   for (lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
     const line = lines[lineNumber];
@@ -85,12 +88,20 @@ export async function extractPackageFile(
     for (const delimiter of delimiters) {
       sourceMatch =
         sourceMatch ??
-        regEx(`^source ${delimiter}([^${delimiter}]+)${delimiter}\\s*$`).exec(
-          line,
-        );
+        regEx(
+          `^source ((${delimiter}(?<registryUrl>[^${delimiter}]+)${delimiter})|(?<sourceName>\\w+))\\s*$`,
+        ).exec(line);
     }
     if (sourceMatch) {
-      res.registryUrls?.push(sourceMatch[1]);
+      if (sourceMatch.groups?.registryUrl) {
+        res.registryUrls?.push(sourceMatch.groups.registryUrl);
+      }
+      if (sourceMatch.groups?.sourceName) {
+        const registryUrl = variables[sourceMatch.groups.sourceName];
+        if (registryUrl) {
+          res.registryUrls?.push(registryUrl);
+        }
+      }
     }
 
     const rubyMatch = extractRubyVersion(line);
@@ -103,8 +114,18 @@ export async function extractPackageFile(
       });
     }
 
+    const variableMatchRegex = regEx(
+      `^(?<key>\\w+)\\s*=\\s*['"](?<value>[^'"]+)['"]`,
+    );
+    const variableMatch = variableMatchRegex.exec(line);
+    if (variableMatch) {
+      if (variableMatch.groups?.key) {
+        variables[variableMatch.groups?.key] = variableMatch.groups?.value;
+      }
+    }
+
     const gemMatchRegex = regEx(
-      `^\\s*gem\\s+(['"])(?<depName>[^'"]+)(['"])(\\s*,\\s*(?<currentValue>(['"])[^'"]+['"](\\s*,\\s*['"][^'"]+['"])?))?`,
+      `^\\s*gem\\s+(['"])(?<depName>[^'"]+)(['"])(\\s*,\\s*(?<currentValue>(['"])[^'"]+['"](\\s*,\\s*['"][^'"]+['"])?))?(\\s*,\\s*source:\\s*(['"](?<registryUrl>[^'"]+)['"]|(?<sourceName>[^'"]+)))?`,
     );
     const gemMatch = gemMatchRegex.exec(line);
     if (gemMatch) {
@@ -124,10 +145,18 @@ export async function extractPackageFile(
 
     for (const delimiter of delimiters) {
       const sourceBlockMatch = regEx(
-        `^source\\s+${delimiter}(.*?)${delimiter}\\s+do`,
+        `^source\\s+((${delimiter}(?<registryUrl>[^${delimiter}]+)${delimiter})|(?<sourceName>\\w+))\\s+do`,
       ).exec(line);
       if (sourceBlockMatch) {
-        const repositoryUrl = sourceBlockMatch[1];
+        let repositoryUrl = '';
+        if (sourceBlockMatch.groups?.registryUrl) {
+          repositoryUrl = sourceBlockMatch.groups.registryUrl;
+        }
+        if (sourceBlockMatch.groups?.sourceName) {
+          if (variables[sourceBlockMatch.groups.sourceName]) {
+            repositoryUrl = variables[sourceBlockMatch.groups.sourceName];
+          }
+        }
         const sourceLineNumber = lineNumber;
         let sourceContent = '';
         let sourceLine = '';
-- 
GitLab