From ee6c40f800108277bd15fd5a45e6a694f7795f6e Mon Sep 17 00:00:00 2001
From: Joshua Tang <joshuaystang@gmail.com>
Date: Mon, 7 Aug 2023 16:04:54 +1000
Subject: [PATCH] fix(manager/pub): extract sdk constraint correctly (#23367)

Co-authored-by: Sergei Zharinov <zharinov@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 lib/modules/manager/pub/artifacts.spec.ts |  1 -
 lib/modules/manager/pub/artifacts.ts      | 22 ++++-------
 lib/modules/manager/pub/schema.ts         | 13 +++++++
 lib/modules/manager/pub/utils.spec.ts     | 30 +++++++++++++++
 lib/modules/manager/pub/utils.ts          | 18 +++++++++
 lib/util/schema-utils.spec.ts             | 45 +++++++++++++++++++++++
 lib/util/schema-utils.ts                  | 10 +++++
 7 files changed, 123 insertions(+), 16 deletions(-)
 create mode 100644 lib/modules/manager/pub/schema.ts
 create mode 100644 lib/modules/manager/pub/utils.spec.ts
 create mode 100644 lib/modules/manager/pub/utils.ts

diff --git a/lib/modules/manager/pub/artifacts.spec.ts b/lib/modules/manager/pub/artifacts.spec.ts
index 2ae1fb8bb7..c7b8aaf903 100644
--- a/lib/modules/manager/pub/artifacts.spec.ts
+++ b/lib/modules/manager/pub/artifacts.spec.ts
@@ -41,7 +41,6 @@ const updateArtifact: UpdateArtifact = {
 describe('modules/manager/pub/artifacts', () => {
   beforeEach(() => {
     jest.resetAllMocks();
-    jest.resetModules();
 
     env.getChildProcessEnv.mockReturnValue(envMock.basic);
     GlobalConfig.set(adminConfig);
diff --git a/lib/modules/manager/pub/artifacts.ts b/lib/modules/manager/pub/artifacts.ts
index 16b4de4b22..613cd364ce 100644
--- a/lib/modules/manager/pub/artifacts.ts
+++ b/lib/modules/manager/pub/artifacts.ts
@@ -9,19 +9,8 @@ import {
   readLocalFile,
   writeLocalFile,
 } from '../../../util/fs';
-import { regEx } from '../../../util/regex';
 import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
-
-function getFlutterConstraint(lockFileContent: string): string | undefined {
-  return regEx(/^\tflutter: ['"](?<flutterVersion>.*)['"]$/m).exec(
-    lockFileContent
-  )?.groups?.flutterVersion;
-}
-
-function getDartConstraint(lockFileContent: string): string | undefined {
-  return regEx(/^\tdart: ['"](?<dartVersion>.*)['"]$/m).exec(lockFileContent)
-    ?.groups?.dartVersion;
-}
+import { parsePubspecLock } from './utils';
 
 export async function updateArtifacts({
   packageFileName,
@@ -63,9 +52,12 @@ export async function updateArtifacts({
       );
     }
 
-    const constraint = isFlutter
-      ? config.constraints?.flutter ?? getFlutterConstraint(oldLockFileContent)
-      : config.constraints?.dart ?? getDartConstraint(oldLockFileContent);
+    let constraint = config.constraints?.[toolName];
+    if (!constraint) {
+      const pubspecLock = parsePubspecLock(lockFileName, oldLockFileContent);
+      constraint = pubspecLock?.sdks[toolName];
+    }
+
     const execOptions: ExecOptions = {
       cwdFile: packageFileName,
       docker: {},
diff --git a/lib/modules/manager/pub/schema.ts b/lib/modules/manager/pub/schema.ts
new file mode 100644
index 0000000000..dbf72be5c7
--- /dev/null
+++ b/lib/modules/manager/pub/schema.ts
@@ -0,0 +1,13 @@
+import { z } from 'zod';
+import { Yaml } from '../../../util/schema-utils';
+
+export const PubspecLockSchema = z.object({
+  sdks: z.object({
+    dart: z.string(),
+    flutter: z.string().optional(),
+  }),
+});
+
+export type PubspecLockSchema = z.infer<typeof PubspecLockSchema>;
+
+export const PubspecLockYaml = Yaml.pipe(PubspecLockSchema);
diff --git a/lib/modules/manager/pub/utils.spec.ts b/lib/modules/manager/pub/utils.spec.ts
new file mode 100644
index 0000000000..c236d4c0be
--- /dev/null
+++ b/lib/modules/manager/pub/utils.spec.ts
@@ -0,0 +1,30 @@
+import { codeBlock } from 'common-tags';
+import { parsePubspecLock } from './utils';
+
+describe('modules/manager/pub/utils', () => {
+  describe('parsePubspeckLock', () => {
+    const fileName = 'pubspec.lock';
+
+    it('load and parse successfully', () => {
+      const pubspecLock = codeBlock`
+        sdks:
+          dart: ">=3.0.0 <4.0.0"
+          flutter: ">=3.10.0"
+      `;
+      const actual = parsePubspecLock(fileName, pubspecLock);
+      expect(actual).toMatchObject({
+        sdks: { dart: '>=3.0.0 <4.0.0', flutter: '>=3.10.0' },
+      });
+    });
+
+    it('invalid yaml', () => {
+      const actual = parsePubspecLock(fileName, 'clearly-invalid');
+      expect(actual).toBeNull();
+    });
+
+    it('invalid schema', () => {
+      const actual = parsePubspecLock(fileName, 'clearly:\n\tinvalid: lock');
+      expect(actual).toBeNull();
+    });
+  });
+});
diff --git a/lib/modules/manager/pub/utils.ts b/lib/modules/manager/pub/utils.ts
new file mode 100644
index 0000000000..7aa7f05e0e
--- /dev/null
+++ b/lib/modules/manager/pub/utils.ts
@@ -0,0 +1,18 @@
+import { logger } from '../../../logger';
+import { PubspecLockSchema, PubspecLockYaml } from './schema';
+
+export function parsePubspecLock(
+  fileName: string,
+  fileContent: string
+): PubspecLockSchema | null {
+  const res = PubspecLockYaml.safeParse(fileContent);
+  if (res.success) {
+    return res.data;
+  } else {
+    logger.debug(
+      { err: res.error, fileName },
+      `Error parsing pubspec lockfile.`
+    );
+  }
+  return null;
+}
diff --git a/lib/util/schema-utils.spec.ts b/lib/util/schema-utils.spec.ts
index 9be7ac651c..c194236ce6 100644
--- a/lib/util/schema-utils.spec.ts
+++ b/lib/util/schema-utils.spec.ts
@@ -6,6 +6,7 @@ import {
   LooseRecord,
   Url,
   UtcDate,
+  Yaml,
 } from './schema-utils';
 
 describe('util/schema-utils', () => {
@@ -295,4 +296,48 @@ describe('util/schema-utils', () => {
       expect(() => Url.parse(urlStr)).toThrow('Invalid URL');
     });
   });
+
+  describe('Yaml', () => {
+    const Schema = Yaml.pipe(
+      z.object({ foo: z.array(z.object({ bar: z.literal('baz') })) })
+    );
+
+    it('parses valid yaml', () => {
+      expect(Schema.parse('foo:\n- bar: baz')).toEqual({
+        foo: [{ bar: 'baz' }],
+      });
+    });
+
+    it('throws error for non-string', () => {
+      expect(Schema.safeParse(42)).toMatchObject({
+        error: {
+          issues: [
+            {
+              message: 'Expected string, received number',
+              code: 'invalid_type',
+              expected: 'string',
+              received: 'number',
+              path: [],
+            },
+          ],
+        },
+        success: false,
+      });
+    });
+
+    it('throws error for invalid yaml', () => {
+      expect(Schema.safeParse('clearly: "invalid" "yaml"')).toMatchObject({
+        error: {
+          issues: [
+            {
+              message: 'Invalid YAML',
+              code: 'custom',
+              path: [],
+            },
+          ],
+        },
+        success: false,
+      });
+    });
+  });
 });
diff --git a/lib/util/schema-utils.ts b/lib/util/schema-utils.ts
index 0caa342e39..e200a04bd1 100644
--- a/lib/util/schema-utils.ts
+++ b/lib/util/schema-utils.ts
@@ -1,3 +1,4 @@
+import { load } from 'js-yaml';
 import JSON5 from 'json5';
 import { DateTime } from 'luxon';
 import type { JsonValue } from 'type-fest';
@@ -232,3 +233,12 @@ export const Url = z.string().transform((str, ctx): URL => {
     return z.NEVER;
   }
 });
+
+export const Yaml = z.string().transform((str, ctx): JsonValue => {
+  try {
+    return load(str, { json: true }) as JsonValue;
+  } catch (e) {
+    ctx.addIssue({ code: 'custom', message: 'Invalid YAML' });
+    return z.NEVER;
+  }
+});
-- 
GitLab