From ed4c2e6b4bd6128d09c3091fb54f5c0174ec6ca6 Mon Sep 17 00:00:00 2001 From: Shunsuke Suzuki Date: Wed, 8 May 2024 16:27:38 +0900 Subject: [PATCH] fix(manager/devcontainer): parse with JSONC parser (#28914) --- .../manager/devcontainer/extract.spec.ts | 36 ++++++++++ lib/modules/manager/devcontainer/schema.ts | 4 +- lib/util/schema-utils.spec.ts | 67 +++++++++++++++++++ lib/util/schema-utils.ts | 11 +++ package.json | 1 + pnpm-lock.yaml | 3 + 6 files changed, 120 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/devcontainer/extract.spec.ts b/lib/modules/manager/devcontainer/extract.spec.ts index 3cd12745ed673f..76b5b28993f898 100644 --- a/lib/modules/manager/devcontainer/extract.spec.ts +++ b/lib/modules/manager/devcontainer/extract.spec.ts @@ -27,6 +27,42 @@ describe('modules/manager/devcontainer/extract', () => { expect(result).toBeNull(); }); + it('tests if JSONC can be parsed', () => { + // Arrange + const content = codeBlock(` + { + // hello + "features": { + "devcontainer.registry.renovate.com/test/features/first:1.2.3": {} + } + }`); + const extractConfig = {}; + // Act + const result = extractPackageFile( + content, + 'devcontainer.json', + extractConfig, + ); + + // Assert + expect(result).toEqual({ + deps: [ + { + autoReplaceStringTemplate: + '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}', + currentDigest: undefined, + currentValue: '1.2.3', + datasource: 'docker', + depName: 'devcontainer.registry.renovate.com/test/features/first', + depType: 'feature', + pinDigests: false, + replaceString: + 'devcontainer.registry.renovate.com/test/features/first:1.2.3', + }, + ], + }); + }); + it('returns feature image deps when only the features property is defined in dev container JSON file', () => { // Arrange const content = codeBlock(` diff --git a/lib/modules/manager/devcontainer/schema.ts b/lib/modules/manager/devcontainer/schema.ts index b798121ed44d44..38498632044b3e 100644 --- a/lib/modules/manager/devcontainer/schema.ts +++ b/lib/modules/manager/devcontainer/schema.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { Json } from '../../../util/schema-utils'; +import { Jsonc } from '../../../util/schema-utils'; -export const DevContainerFile = Json.pipe( +export const DevContainerFile = Jsonc.pipe( z.object({ image: z.string().optional(), features: z.record(z.unknown()).optional(), diff --git a/lib/util/schema-utils.spec.ts b/lib/util/schema-utils.spec.ts index 390c2ed2bb0832..ccca2f70b9e8e1 100644 --- a/lib/util/schema-utils.spec.ts +++ b/lib/util/schema-utils.spec.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { Json, Json5, + Jsonc, LooseArray, LooseRecord, MultidocYaml, @@ -269,6 +270,72 @@ describe('util/schema-utils', () => { }); }); + describe('Jsonc', () => { + it('parses JSONC', () => { + const Schema = Jsonc.pipe(z.object({ foo: z.literal('bar') })); + + expect(Schema.parse('{"foo": "bar"}')).toEqual({ foo: 'bar' }); + + expect(Schema.safeParse(42)).toMatchObject({ + error: { + issues: [ + { + message: 'Expected string, received number', + code: 'invalid_type', + expected: 'string', + received: 'number', + path: [], + }, + ], + }, + success: false, + }); + + expect(Schema.safeParse('{"foo": "foo"}')).toMatchObject({ + error: { + issues: [ + { + message: 'Invalid literal value, expected "bar"', + code: 'invalid_literal', + expected: 'bar', + received: 'foo', + path: ['foo'], + }, + ], + }, + success: false, + }); + + expect(Schema.safeParse('["foo", "bar"]')).toMatchObject({ + error: { + issues: [ + { + message: 'Expected object, received array', + code: 'invalid_type', + expected: 'object', + received: 'array', + path: [], + }, + ], + }, + success: false, + }); + + expect(Schema.safeParse('{')).toMatchObject({ + error: { + issues: [ + { + message: 'Invalid JSONC', + code: 'custom', + path: [], + }, + ], + }, + success: false, + }); + }); + }); + describe('UtcDate', () => { it('parses date', () => { expect(UtcDate.parse('2020-04-04').toString()).toBe( diff --git a/lib/util/schema-utils.ts b/lib/util/schema-utils.ts index b0c32ad60f6ce6..1f7cb3d6da15ad 100644 --- a/lib/util/schema-utils.ts +++ b/lib/util/schema-utils.ts @@ -1,4 +1,5 @@ import JSON5 from 'json5'; +import * as JSONC from 'jsonc-parser'; import { DateTime } from 'luxon'; import type { JsonArray, JsonValue } from 'type-fest'; import { z } from 'zod'; @@ -215,6 +216,16 @@ export const Json5 = z.string().transform((str, ctx): JsonValue => { } }); +export const Jsonc = z.string().transform((str, ctx): JsonValue => { + const errors: JSONC.ParseError[] = []; + const value = JSONC.parse(str, errors); + if (errors.length === 0) { + return value; + } + ctx.addIssue({ code: 'custom', message: 'Invalid JSONC' }); + return z.NEVER; +}); + export const UtcDate = z .string({ description: 'ISO 8601 string' }) .transform((str, ctx): DateTime => { diff --git a/package.json b/package.json index 4e92c6b893eb3b..4d4774fc9479b0 100644 --- a/package.json +++ b/package.json @@ -210,6 +210,7 @@ "json-stringify-pretty-compact": "3.0.0", "json5": "2.2.3", "jsonata": "2.0.4", + "jsonc-parser": "3.2.1", "klona": "2.0.6", "lru-cache": "10.2.2", "luxon": "3.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdea0c105610ce..c49003771c2826 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,6 +215,9 @@ importers: jsonata: specifier: 2.0.4 version: 2.0.4 + jsonc-parser: + specifier: 3.2.1 + version: 3.2.1 klona: specifier: 2.0.6 version: 2.0.6