From 2336161d05a3e4ff0b950e10d955c6b590d83e26 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Sun, 5 Feb 2023 10:20:07 +0300
Subject: [PATCH] fix(packagist): Use schema for `extractDepReleases` method
 (#20034)

---
 .../datasource/packagist/index.spec.ts        |  8 +--
 lib/modules/datasource/packagist/index.ts     | 44 +++++++-------
 .../datasource/packagist/schema.spec.ts       | 59 +++++++++++++++----
 lib/modules/datasource/packagist/schema.ts    | 41 +++++++++----
 lib/modules/datasource/packagist/types.ts     |  2 +-
 5 files changed, 104 insertions(+), 50 deletions(-)

diff --git a/lib/modules/datasource/packagist/index.spec.ts b/lib/modules/datasource/packagist/index.spec.ts
index 4ef75442e1..88d6775fe1 100644
--- a/lib/modules/datasource/packagist/index.spec.ts
+++ b/lib/modules/datasource/packagist/index.spec.ts
@@ -57,10 +57,10 @@ describe('modules/datasource/packagist/index', () => {
       const packagesOnly = {
         packages: {
           'vendor/package-name': {
-            'dev-master': {},
-            '1.0.x-dev': {},
-            '0.0.1': {},
-            '1.0.0': {},
+            'dev-master': { version: 'dev-master' },
+            '1.0.x-dev': { version: '1.0.x-dev' },
+            '0.0.1': { version: '0.0.1' },
+            '1.0.0': { version: '1.0.0' },
           },
         },
       };
diff --git a/lib/modules/datasource/packagist/index.ts b/lib/modules/datasource/packagist/index.ts
index eb4a57056a..43024bbb3b 100644
--- a/lib/modules/datasource/packagist/index.ts
+++ b/lib/modules/datasource/packagist/index.ts
@@ -5,7 +5,6 @@ import { cache } from '../../../util/cache/package/decorator';
 import * as hostRules from '../../../util/host-rules';
 import type { HttpOptions } from '../../../util/http/types';
 import * as p from '../../../util/promises';
-import { regEx } from '../../../util/regex';
 import { ensureTrailingSlash, joinUrlParts } from '../../../util/url';
 import * as composerVersioning from '../../versioning/composer';
 import { Datasource } from '../datasource';
@@ -119,27 +118,28 @@ export class PackagistDatasource extends Datasource {
     return packagistFile;
   }
 
-  private static extractDepReleases(versions: RegistryFile): ReleaseResult {
-    const dep: ReleaseResult = { releases: [] };
-    // istanbul ignore if
-    if (!versions) {
-      return dep;
+  /* istanbul ignore next */
+  private static extractDepReleases(
+    composerReleases: unknown
+  ): ReleaseResult | null {
+    const parsedRecord =
+      schema.ComposerReleasesRecord.safeParse(composerReleases);
+    if (parsedRecord.success) {
+      return schema.extractReleaseResult(Object.values(parsedRecord.data));
     }
-    dep.releases = Object.keys(versions).map((version) => {
-      // TODO: fix function parameter type: `versions`
-      const release = (versions as any)[version];
-      const parsedVersion = release.version ?? version;
-      dep.homepage = release.homepage || dep.homepage;
-      if (release.source?.url) {
-        dep.sourceUrl = release.source.url;
-      }
-      return {
-        version: parsedVersion.replace(regEx(/^v/), ''),
-        gitRef: parsedVersion,
-        releaseTimestamp: release.time,
-      };
-    });
-    return dep;
+
+    const parsedArray =
+      schema.ComposerReleasesArray.safeParse(composerReleases);
+    if (parsedArray.success) {
+      logger.once.info('Packagist: extracting releases from array');
+      return schema.extractReleaseResult(parsedArray.data);
+    }
+
+    logger.once.info(
+      { composerReleases },
+      'Packagist: unknown format to extract from'
+    );
+    return null;
   }
 
   @cache({
@@ -162,7 +162,7 @@ export class PackagistDatasource extends Datasource {
       providerPackages,
     } = registryMeta;
 
-    const includesPackages: Record<string, ReleaseResult> = {};
+    const includesPackages: Record<string, ReleaseResult | null> = {};
 
     const tasks: (() => Promise<void>)[] = [];
 
diff --git a/lib/modules/datasource/packagist/schema.spec.ts b/lib/modules/datasource/packagist/schema.spec.ts
index 4a4cca0390..0180a1ab9c 100644
--- a/lib/modules/datasource/packagist/schema.spec.ts
+++ b/lib/modules/datasource/packagist/schema.spec.ts
@@ -1,7 +1,8 @@
 import type { ReleaseResult } from '../types';
 import {
   ComposerRelease,
-  ComposerReleases,
+  ComposerReleasesArray,
+  ComposerReleasesRecord,
   MinifiedArray,
   parsePackagesResponse,
   parsePackagesResponses,
@@ -110,25 +111,57 @@ describe('modules/datasource/packagist/schema', () => {
     });
   });
 
-  describe('ComposerReleases', () => {
-    it('rejects ComposerReleases', () => {
-      expect(() => ComposerReleases.parse(null)).toThrow();
-      expect(() => ComposerReleases.parse(undefined)).toThrow();
-      expect(() => ComposerReleases.parse('')).toThrow();
-      expect(() => ComposerReleases.parse({})).toThrow();
+  describe('ComposerReleasesArray', () => {
+    it('rejects ComposerReleasesArray', () => {
+      expect(() => ComposerReleasesArray.parse(null)).toThrow();
+      expect(() => ComposerReleasesArray.parse(undefined)).toThrow();
+      expect(() => ComposerReleasesArray.parse('')).toThrow();
+      expect(() => ComposerReleasesArray.parse({})).toThrow();
     });
 
-    it('parses ComposerReleases', () => {
-      expect(ComposerReleases.parse([])).toEqual([]);
-      expect(ComposerReleases.parse([null])).toEqual([]);
-      expect(ComposerReleases.parse([1, 2, 3])).toEqual([]);
-      expect(ComposerReleases.parse(['foobar'])).toEqual([]);
+    it('parses ComposerReleasesArray', () => {
+      expect(ComposerReleasesArray.parse([])).toEqual([]);
+      expect(ComposerReleasesArray.parse([null])).toEqual([]);
+      expect(ComposerReleasesArray.parse([1, 2, 3])).toEqual([]);
+      expect(ComposerReleasesArray.parse(['foobar'])).toEqual([]);
       expect(
-        ComposerReleases.parse([{ version: '1.2.3' }, { version: 'dev-main' }])
+        ComposerReleasesArray.parse([
+          { version: '1.2.3' },
+          { version: 'dev-main' },
+        ])
       ).toEqual([{ version: '1.2.3' }, { version: 'dev-main' }]);
     });
   });
 
+  describe('ComposerReleasesRecord', () => {
+    it('rejects ComposerReleasesRecord', () => {
+      expect(() => ComposerReleasesRecord.parse(null)).toThrow();
+      expect(() => ComposerReleasesRecord.parse(undefined)).toThrow();
+      expect(() => ComposerReleasesRecord.parse('')).toThrow();
+      expect(() => ComposerReleasesRecord.parse([])).toThrow();
+    });
+
+    it('parses ComposerReleasesRecord', () => {
+      expect(ComposerReleasesRecord.parse({})).toEqual({});
+      expect(ComposerReleasesRecord.parse({ foo: null })).toEqual({});
+      expect(ComposerReleasesRecord.parse({ foo: 1, bar: 2, baz: 3 })).toEqual(
+        {}
+      );
+      expect(ComposerReleasesRecord.parse({ foo: 'bar' })).toEqual({});
+      expect(
+        ComposerReleasesRecord.parse({
+          '0.0.1': { foo: 'bar' },
+          '0.0.2': { version: '0.0.1' },
+          '1.2.3': { version: '1.2.3' },
+          'dev-main': { version: 'dev-main' },
+        })
+      ).toEqual({
+        '1.2.3': { version: '1.2.3' },
+        'dev-main': { version: 'dev-main' },
+      });
+    });
+  });
+
   describe('parsePackageResponse', () => {
     it('parses package response', () => {
       expect(parsePackagesResponse('foo/bar', null)).toEqual([]);
diff --git a/lib/modules/datasource/packagist/schema.ts b/lib/modules/datasource/packagist/schema.ts
index 4a5f379e12..5f26496fee 100644
--- a/lib/modules/datasource/packagist/schema.ts
+++ b/lib/modules/datasource/packagist/schema.ts
@@ -65,10 +65,23 @@ export const ComposerRelease = z
   );
 export type ComposerRelease = z.infer<typeof ComposerRelease>;
 
-export const ComposerReleases = z
+export const ComposerReleasesArray = z
   .array(ComposerRelease.nullable().catch(null))
   .transform((xs) => xs.filter((x): x is ComposerRelease => x !== null));
-export type ComposerReleases = z.infer<typeof ComposerReleases>;
+export type ComposerReleasesArray = z.infer<typeof ComposerReleasesArray>;
+
+export const ComposerReleasesRecord = z
+  .record(ComposerRelease.nullable().catch(null))
+  .transform((map) => {
+    const res: Record<string, ComposerRelease> = {};
+    for (const [key, value] of Object.entries(map)) {
+      if (value !== null && value.version === key) {
+        res[key] = value;
+      }
+    }
+    return res;
+  });
+export type ComposerReleasesRecord = z.infer<typeof ComposerReleasesRecord>;
 
 export const ComposerPackagesResponse = z.object({
   packages: z.record(z.unknown()),
@@ -77,11 +90,11 @@ export const ComposerPackagesResponse = z.object({
 export function parsePackagesResponse(
   packageName: string,
   packagesResponse: unknown
-): ComposerReleases {
+): ComposerReleasesArray {
   try {
     const { packages } = ComposerPackagesResponse.parse(packagesResponse);
     const array = MinifiedArray.parse(packages[packageName]);
-    const releases = ComposerReleases.parse(array);
+    const releases = ComposerReleasesArray.parse(array);
     return releases;
   } catch (err) {
     logger.debug(
@@ -92,17 +105,15 @@ export function parsePackagesResponse(
   }
 }
 
-export function parsePackagesResponses(
-  packageName: string,
-  packagesResponses: unknown[]
+export function extractReleaseResult(
+  ...composerReleasesArrays: ComposerReleasesArray[]
 ): ReleaseResult | null {
   const releases: Release[] = [];
   let homepage: string | null | undefined;
   let sourceUrl: string | null | undefined;
 
-  for (const packagesResponse of packagesResponses) {
-    const releaseArray = parsePackagesResponse(packageName, packagesResponse);
-    for (const composerRelease of releaseArray) {
+  for (const composerReleasesArray of composerReleasesArrays) {
+    for (const composerRelease of composerReleasesArray) {
       const version = composerRelease.version.replace(/^v/, '');
       const gitRef = composerRelease.version;
 
@@ -144,3 +155,13 @@ export function parsePackagesResponses(
 
   return result;
 }
+
+export function parsePackagesResponses(
+  packageName: string,
+  packagesResponses: unknown[]
+): ReleaseResult | null {
+  const releaseArrays = packagesResponses.map((pkgResp) =>
+    parsePackagesResponse(packageName, pkgResp)
+  );
+  return extractReleaseResult(...releaseArrays);
+}
diff --git a/lib/modules/datasource/packagist/types.ts b/lib/modules/datasource/packagist/types.ts
index fea795612e..73948fe1df 100644
--- a/lib/modules/datasource/packagist/types.ts
+++ b/lib/modules/datasource/packagist/types.ts
@@ -32,5 +32,5 @@ export interface AllPackages {
   providersLazyUrl?: string;
   providerPackages: Record<string, string>;
 
-  includesPackages: Record<string, ReleaseResult>;
+  includesPackages: Record<string, ReleaseResult | null>;
 }
-- 
GitLab