From 2a809f4d98ba10b10913fb2ed6ea421c5663f44a Mon Sep 17 00:00:00 2001 From: Tobias Schlatter <schlatter.tobias@gmail.com> Date: Wed, 12 Feb 2025 17:32:27 +0100 Subject: [PATCH] refactor(bazel-module): use bazel syntactic concepts in parser (#34154) --- .../manager/bazel-module/context.spec.ts | 80 ++++++++++++++---- lib/modules/manager/bazel-module/context.ts | 79 +++++++++++++----- lib/modules/manager/bazel-module/extract.ts | 8 +- .../manager/bazel-module/fragments.spec.ts | 71 ++++++++++++---- lib/modules/manager/bazel-module/fragments.ts | 70 ++++++++++++++-- .../bazel-module/parser/extension-tags.ts | 50 +++++++++++ .../manager/bazel-module/parser/index.spec.ts | 74 ++++++++++------- .../manager/bazel-module/parser/index.ts | 11 ++- .../manager/bazel-module/parser/maven.ts | 83 +++++++------------ .../manager/bazel-module/parser/oci.ts | 35 +++----- .../parser/{module.ts => rules.ts} | 12 ++- .../manager/bazel-module/rules.spec.ts | 39 ++++----- lib/modules/manager/bazel-module/rules.ts | 56 ++++++------- 13 files changed, 432 insertions(+), 236 deletions(-) create mode 100644 lib/modules/manager/bazel-module/parser/extension-tags.ts rename lib/modules/manager/bazel-module/parser/{module.ts => rules.ts} (57%) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index a134bb69fe..2cf9c71a9f 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -13,9 +13,9 @@ describe('modules/manager/bazel-module/context', () => { .endRule(); expect(ctx.results).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }, @@ -32,9 +32,9 @@ describe('modules/manager/bazel-module/context', () => { .endRule(); expect(ctx.results).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), }, true, @@ -55,9 +55,9 @@ describe('modules/manager/bazel-module/context', () => { .endRule(); expect(ctx.results).toEqual([ - fragments.record( + fragments.rule( + 'foo_library', { - rule: fragments.string('foo_library'), name: fragments.string('my_library'), srcs: fragments.array( [fragments.string('first'), fragments.string('second')], @@ -69,23 +69,73 @@ describe('modules/manager/bazel-module/context', () => { ]); }); - describe('.currentRecord', () => { + it('construct an extension tag', () => { + const ctx = new Ctx() + .prepareExtensionTag('maven', 'maven_01') + .startExtensionTag('install') + .startAttribute('artifacts') + .startArray() + .addString('org.example:my-lib:1.0.0') + .endArray() + .endExtensionTag(); + + expect(ctx.results).toEqual([ + fragments.extensionTag( + 'maven', + 'maven_01', + 'install', + { + artifacts: fragments.array( + [fragments.string('org.example:my-lib:1.0.0')], + true, + ), + }, + true, + ), + ]); + }); + + describe('extension tag failure cases', () => { + it('throws if there is no current', () => { + expect(() => new Ctx().startExtensionTag('install')).toThrow( + new Error('Requested current, but no value.'), + ); + }); + + it('throws if the current is not a prepared extension tag', () => { + expect(() => + new Ctx().startRule('foo').startExtensionTag('install'), + ).toThrow( + new Error( + 'Requested current prepared extension tag, but does not exist.', + ), + ); + }); + + it('throws if the current is not an extension tag', () => { + expect(() => new Ctx().startRule('foo').endExtensionTag()).toThrow( + new Error('Requested current extension tag, but does not exist.'), + ); + }); + }); + + describe('.currentRule', () => { it('returns the record fragment if it is current', () => { - const ctx = new Ctx().startRecord(); - expect(ctx.currentRecord).toEqual(fragments.record()); + const ctx = new Ctx().startRule('dummy'); + expect(ctx.currentRule).toEqual(fragments.rule('dummy')); }); it('throws if there is no current', () => { const ctx = new Ctx(); - expect(() => ctx.currentRecord).toThrow( + expect(() => ctx.currentRule).toThrow( new Error('Requested current, but no value.'), ); }); - it('throws if the current is not a record fragment', () => { + it('throws if the current is not a rule fragment', () => { const ctx = new Ctx().startArray(); - expect(() => ctx.currentRecord).toThrow( - new Error('Requested current record, but does not exist.'), + expect(() => ctx.currentRule).toThrow( + new Error('Requested current rule, but does not exist.'), ); }); }); @@ -96,8 +146,8 @@ describe('modules/manager/bazel-module/context', () => { expect(ctx.currentArray).toEqual(fragments.array()); }); - it('throws if the current is not a record fragment', () => { - const ctx = new Ctx().startRecord(); + it('throws if the current is not an array fragment', () => { + const ctx = new Ctx().startRule('dummy'); expect(() => ctx.currentArray).toThrow( new Error('Requested current array, but does not exist.'), ); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index b590a560e0..6ad1a20ea1 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -1,14 +1,16 @@ import type { AllFragments, ArrayFragment, - ChildFragments, - RecordFragment, + ExtensionTagFragment, + PreparedExtensionTagFragment, + ResultFragment, + RuleFragment, } from './fragments'; import * as fragments from './fragments'; // Represents the fields that the context must have. export interface CtxCompatible { - results: RecordFragment[]; + results: ResultFragment[]; stack: AllFragments[]; } @@ -27,12 +29,12 @@ export class CtxProcessingError extends Error { } export class Ctx implements CtxCompatible { - results: RecordFragment[]; + results: ResultFragment[]; stack: AllFragments[]; - constructor(results: RecordFragment[] = [], stack: AllFragments[] = []) { - this.results = results; - this.stack = stack; + constructor() { + this.results = []; + this.stack = []; } private get safeCurrent(): AllFragments | undefined { @@ -46,12 +48,20 @@ export class Ctx implements CtxCompatible { } return c; } - get currentRecord(): RecordFragment { + get currentRule(): RuleFragment { const current = this.current; - if (current.type === 'record') { + if (current.type === 'rule') { return current; } - throw new Error('Requested current record, but does not exist.'); + throw new Error('Requested current rule, but does not exist.'); + } + + get currentExtensionTag(): ExtensionTagFragment { + const current = this.current; + if (current.type === 'extensionTag') { + return current; + } + throw new Error('Requested current extension tag, but does not exist.'); } get currentArray(): ArrayFragment { @@ -62,6 +72,19 @@ export class Ctx implements CtxCompatible { throw new Error('Requested current array, but does not exist.'); } + private popPreparedExtensionTag(): PreparedExtensionTagFragment { + const c = this.stack.pop(); + if (c === undefined) { + throw new Error('Requested current, but no value.'); + } + if (c.type === 'preparedExtensionTag') { + return c; + } + throw new Error( + 'Requested current prepared extension tag, but does not exist.', + ); + } + private popStack(): boolean { const current = this.stack.pop(); if (!current) { @@ -84,14 +107,14 @@ export class Ctx implements CtxCompatible { return true; } if ( - parent.type === 'record' && + (parent.type === 'rule' || parent.type === 'extensionTag') && current.type === 'attribute' && current.value !== undefined ) { parent.children[current.name] = current.value; return true; } - } else if (current.type === 'record') { + } else if (current.type === 'rule' || current.type === 'extensionTag') { this.results.push(current); return true; } @@ -116,24 +139,36 @@ export class Ctx implements CtxCompatible { return this.processStack(); } - startRecord(children: ChildFragments = {}): Ctx { - const record = fragments.record(children); - this.stack.push(record); + startRule(name: string): Ctx { + const rule = fragments.rule(name); + this.stack.push(rule); return this; } - endRecord(): Ctx { - const record = this.currentRecord; - record.isComplete = true; + endRule(): Ctx { + const rule = this.currentRule; + rule.isComplete = true; return this.processStack(); } - startRule(name: string): Ctx { - return this.startRecord({ rule: fragments.string(name) }); + prepareExtensionTag(extension: string, rawExtension: string): Ctx { + const preppedTag = fragments.preparedExtensionTag(extension, rawExtension); + this.stack.push(preppedTag); + return this; } - endRule(): Ctx { - return this.endRecord(); + startExtensionTag(tag: string): Ctx { + const { extension, rawExtension } = this.popPreparedExtensionTag(); + + const extensionTag = fragments.extensionTag(extension, rawExtension, tag); + this.stack.push(extensionTag); + return this; + } + + endExtensionTag(): Ctx { + const tag = this.currentExtensionTag; + tag.isComplete = true; + return this.processStack(); } startAttribute(name: string): Ctx { diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 8774057c32..ce16ba3c17 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -4,7 +4,7 @@ import { isNotNullOrUndefined } from '../../../util/array'; import { LooseArray } from '../../../util/schema-utils'; import type { PackageDependency, PackageFileContent } from '../types'; import * as bazelrc from './bazelrc'; -import type { RecordFragment } from './fragments'; +import type { ResultFragment } from './fragments'; import { parse } from './parser'; import { RuleToMavenPackageDep, fillRegistryUrls } from './parser/maven'; import { RuleToDockerPackageDep } from './parser/oci'; @@ -45,7 +45,7 @@ export async function extractPackageFile( } async function extractBazelPfc( - records: RecordFragment[], + records: ResultFragment[], packageFile: string, ): Promise<PackageFileContent> { const pfc: PackageFileContent = LooseArray(RuleToBazelModulePackageDep) @@ -66,12 +66,12 @@ async function extractBazelPfc( } function extractGitRepositoryDeps( - records: RecordFragment[], + records: ResultFragment[], ): PackageDependency[] { return LooseArray(GitRepositoryToPackageDep).parse(records); } -function extractMavenDeps(records: RecordFragment[]): PackageDependency[] { +function extractMavenDeps(records: ResultFragment[]): PackageDependency[] { return LooseArray(RuleToMavenPackageDep) .transform(fillRegistryUrls) .parse(records); diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index bf30857cee..e352b35898 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -2,7 +2,9 @@ import { ArrayFragmentSchema, AttributeFragmentSchema, BooleanFragmentSchema, - RecordFragmentSchema, + ExtensionTagFragmentSchema, + PreparedExtensionTagFragmentSchema, + RuleFragmentSchema, StringFragmentSchema, } from './fragments'; import * as fragments from './fragments'; @@ -20,13 +22,46 @@ describe('modules/manager/bazel-module/fragments', () => { expect(result.value).toBe(true); }); - it('.record()', () => { - const result = fragments.record({ name: fragments.string('foo') }, true); - expect(() => RecordFragmentSchema.parse(result)).not.toThrow(); - expect(result.children).toEqual({ name: fragments.string('foo') }); + it('.rule()', () => { + const result = fragments.rule( + 'foo', + { name: fragments.string('bar') }, + true, + ); + expect(() => RuleFragmentSchema.parse(result)).not.toThrow(); + expect(result.rule).toBe('foo'); + expect(result.children).toEqual({ name: fragments.string('bar') }); expect(result.isComplete).toBe(true); }); + it('.extensionTag()', () => { + const result = fragments.extensionTag( + 'ext', + 'ext_01', + 'tag', + { name: fragments.string('bar') }, + true, + ); + + expect(() => ExtensionTagFragmentSchema.parse(result)).not.toThrow(); + expect(result.extension).toBe('ext'); + expect(result.rawExtension).toBe('ext_01'); + expect(result.tag).toBe('tag'); + expect(result.children).toEqual({ name: fragments.string('bar') }); + expect(result.isComplete).toBe(true); + }); + + it('.preparedExtensionTag()', () => { + const result = fragments.preparedExtensionTag('ext', 'ext_01'); + + expect(() => + PreparedExtensionTagFragmentSchema.parse(result), + ).not.toThrow(); + expect(result.extension).toBe('ext'); + expect(result.rawExtension).toBe('ext_01'); + expect(result.isComplete).toBe(false); + }); + it('.attribute()', () => { const result = fragments.attribute('name', fragments.string('foo'), true); expect(() => AttributeFragmentSchema.parse(result)).not.toThrow(); @@ -43,22 +78,26 @@ describe('modules/manager/bazel-module/fragments', () => { }); it.each` - a | exp - ${fragments.string('hello')} | ${true} - ${fragments.boolean(true)} | ${true} - ${fragments.array()} | ${true} - ${fragments.record()} | ${false} + a | exp + ${fragments.string('hello')} | ${true} + ${fragments.boolean(true)} | ${true} + ${fragments.array()} | ${true} + ${fragments.rule('dummy')} | ${false} + ${fragments.extensionTag('ext', 'ext_01', 'tag')} | ${false} + ${fragments.preparedExtensionTag('ext', 'ext_01')} | ${false} `('.isValue($a)', ({ a, exp }) => { expect(fragments.isValue(a)).toBe(exp); }); it.each` - a | exp - ${fragments.string('hello')} | ${true} - ${fragments.boolean(true)} | ${true} - ${fragments.array()} | ${false} - ${fragments.record()} | ${false} - `('.isValue($a)', ({ a, exp }) => { + a | exp + ${fragments.string('hello')} | ${true} + ${fragments.boolean(true)} | ${true} + ${fragments.array()} | ${false} + ${fragments.rule('dummy')} | ${false} + ${fragments.extensionTag('ext', 'ext_01', 'tag')} | ${false} + ${fragments.preparedExtensionTag('ext', 'ext_01')} | ${false} + `('.isPrimitive($a)', ({ a, exp }) => { expect(fragments.isPrimitive(a)).toBe(exp); }); }); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 806a9beb15..4d686d2288 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -31,8 +31,26 @@ const ValueFragmentsSchema = z.discriminatedUnion('type', [ BooleanFragmentSchema, ArrayFragmentSchema, ]); -export const RecordFragmentSchema = z.object({ - type: z.literal('record'), +export const RuleFragmentSchema = z.object({ + type: z.literal('rule'), + rule: z.string(), + children: LooseRecord(ValueFragmentsSchema), + isComplete: z.boolean(), +}); +export const PreparedExtensionTagFragmentSchema = z.object({ + type: z.literal('preparedExtensionTag'), + // See ExtensionTagFragmentSchema for documentation of the fields. + extension: z.string(), + rawExtension: z.string(), + isComplete: z.literal(false), // never complete, parser internal type. +}); +export const ExtensionTagFragmentSchema = z.object({ + type: z.literal('extensionTag'), + // The "logical" name of the extension (e.g. `oci` or `maven`). + extension: z.string(), + // The "raw" name of the extension as it appears in the MODULE file (e.g. `maven_01` or `maven`) + rawExtension: z.string(), + tag: z.string(), children: LooseRecord(ValueFragmentsSchema), isComplete: z.boolean(), }); @@ -46,7 +64,9 @@ export const AllFragmentsSchema = z.discriminatedUnion('type', [ ArrayFragmentSchema, AttributeFragmentSchema, BooleanFragmentSchema, - RecordFragmentSchema, + RuleFragmentSchema, + PreparedExtensionTagFragmentSchema, + ExtensionTagFragmentSchema, StringFragmentSchema, ]); @@ -56,9 +76,14 @@ export type AttributeFragment = z.infer<typeof AttributeFragmentSchema>; export type BooleanFragment = z.infer<typeof BooleanFragmentSchema>; export type ChildFragments = Record<string, ValueFragments>; export type PrimitiveFragments = z.infer<typeof PrimitiveFragmentsSchema>; -export type RecordFragment = z.infer<typeof RecordFragmentSchema>; +export type RuleFragment = z.infer<typeof RuleFragmentSchema>; +export type PreparedExtensionTagFragment = z.infer< + typeof PreparedExtensionTagFragmentSchema +>; +export type ExtensionTagFragment = z.infer<typeof ExtensionTagFragmentSchema>; export type StringFragment = z.infer<typeof StringFragmentSchema>; export type ValueFragments = z.infer<typeof ValueFragmentsSchema>; +export type ResultFragment = RuleFragment | ExtensionTagFragment; export function string(value: string): StringFragment { return { @@ -76,12 +101,43 @@ export function boolean(value: string | boolean): BooleanFragment { }; } -export function record( +export function rule( + rule: string, + children: ChildFragments = {}, + isComplete = false, +): RuleFragment { + return { + type: 'rule', + rule, + isComplete, + children, + }; +} + +export function preparedExtensionTag( + extension: string, + rawExtension: string, +): PreparedExtensionTagFragment { + return { + type: 'preparedExtensionTag', + extension, + rawExtension, + isComplete: false, // never complete + }; +} + +export function extensionTag( + extension: string, + rawExtension: string, + tag: string, children: ChildFragments = {}, isComplete = false, -): RecordFragment { +): ExtensionTagFragment { return { - type: 'record', + type: 'extensionTag', + extension, + rawExtension, + tag, isComplete, children, }; diff --git a/lib/modules/manager/bazel-module/parser/extension-tags.ts b/lib/modules/manager/bazel-module/parser/extension-tags.ts new file mode 100644 index 0000000000..231112c091 --- /dev/null +++ b/lib/modules/manager/bazel-module/parser/extension-tags.ts @@ -0,0 +1,50 @@ +import { query as q } from 'good-enough-parser'; +import { regEx } from '../../../../util/regex'; +import type { Ctx } from '../context'; +import { kvParams } from './common'; + +import { mavenExtensionPrefix, mavenExtensionTags } from './maven'; +import { ociExtensionPrefix, ociExtensionTags } from './oci'; + +// In bazel modules an extension tag is (roughly) a "member function application". +// For example: +// +// oci = use_extension("@rules_oci//oci:extensions.bzl", "oci") +// ^^^ --> the extension definition (not parsed by this module) +// +// oci.pull(<parameters>) +// ^^^^ --> the extension tag +// +// The name of the extension (`oci` in the example above) technically arbitrary. +// However, in practice, there are conventions. We use this to simplify parsing +// by assuming the extension names start with well-known prefixes. + +const supportedExtensionRegex = regEx( + `^(${ociExtensionPrefix}|${mavenExtensionPrefix}).*$`, +); + +const supportedExtensionTags = [...mavenExtensionTags, ...ociExtensionTags]; + +const supportedExtensionTagsRegex = regEx( + `^(${supportedExtensionTags.join('|')})$`, +); + +export const extensionTags = q + .sym<Ctx>(supportedExtensionRegex, (ctx, token) => { + const rawExtension = token.value; + const match = rawExtension.match(supportedExtensionRegex)!; + const extension = match[1]; + return ctx.prepareExtensionTag(extension, rawExtension); + }) + .op('.') + .sym(supportedExtensionTagsRegex, (ctx, token) => { + return ctx.startExtensionTag(token.value); + }) + .join( + q.tree({ + type: 'wrapped-tree', + maxDepth: 1, + search: kvParams, + postHandler: (ctx) => ctx.endExtensionTag(), + }), + ); diff --git a/lib/modules/manager/bazel-module/parser/index.spec.ts b/lib/modules/manager/bazel-module/parser/index.spec.ts index f3783f8777..cbb629ed0a 100644 --- a/lib/modules/manager/bazel-module/parser/index.spec.ts +++ b/lib/modules/manager/bazel-module/parser/index.spec.ts @@ -21,17 +21,17 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }, true, ), - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_bar'), version: fragments.string('1.0.0'), dev_dependency: fragments.boolean(true), @@ -54,17 +54,17 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }, true, ), - fragments.record( + fragments.rule( + 'git_override', { - rule: fragments.string('git_override'), module_name: fragments.string('rules_foo'), patches: fragments.array( [fragments.string('//:rules_foo.patch')], @@ -94,17 +94,17 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }, true, ), - fragments.record( + fragments.rule( + 'archive_override', { - rule: fragments.string('archive_override'), module_name: fragments.string('rules_foo'), urls: fragments.array( [fragments.string('https://example.com/archive.tar.gz')], @@ -126,17 +126,17 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }, true, ), - fragments.record( + fragments.rule( + 'local_path_override', { - rule: fragments.string('local_path_override'), module_name: fragments.string('rules_foo'), urls: fragments.string('/path/to/repo'), }, @@ -156,17 +156,17 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.rule( + 'bazel_dep', { - rule: fragments.string('bazel_dep'), name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }, true, ), - fragments.record( + fragments.rule( + 'single_version_override', { - rule: fragments.string('single_version_override'), module_name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), registry: fragments.string('https://example.com/custom_registry'), @@ -193,9 +193,11 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.extensionTag( + 'maven', + 'maven', + 'artifact', { - rule: fragments.string('maven_artifact'), group: fragments.string('org.clojure'), artifact: fragments.string('core.specs.alpha'), version: fragments.string('0.2.56'), @@ -212,9 +214,11 @@ describe('modules/manager/bazel-module/parser/index', () => { }, true, ), - fragments.record( + fragments.extensionTag( + 'maven', + 'maven_1', + 'artifact', { - rule: fragments.string('maven_artifact'), group: fragments.string('org.clojure1'), artifact: fragments.string('core.specs.alpha1'), version: fragments.string('0.2.561'), @@ -244,9 +248,11 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.extensionTag( + 'maven', + 'maven', + 'install', { - rule: fragments.string('maven_install'), artifacts: fragments.array( [ { @@ -275,9 +281,11 @@ describe('modules/manager/bazel-module/parser/index', () => { }, true, ), - fragments.record( + fragments.extensionTag( + 'maven', + 'maven', + 'artifact', { - rule: fragments.string('maven_artifact'), group: fragments.string('org.clojure'), artifact: fragments.string('core.specs.alpha'), version: fragments.string('0.2.56'), @@ -300,9 +308,11 @@ describe('modules/manager/bazel-module/parser/index', () => { const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.extensionTag( + 'oci', + 'oci', + 'pull', { - rule: fragments.string('oci_pull'), name: fragments.string('nginx_image'), digest: fragments.string( 'sha256:287ff321f9e3cde74b600cc26197424404157a72043226cbbf07ee8304a2c720', @@ -328,9 +338,9 @@ describe('modules/manager/bazel-module/parser/index', () => { `; const res = parse(input); expect(res).toEqual([ - fragments.record( + fragments.rule( + 'git_repository', { - rule: fragments.string('git_repository'), name: fragments.string('rules_foo'), patches: fragments.array( [fragments.string('//:rules_foo.patch')], diff --git a/lib/modules/manager/bazel-module/parser/index.ts b/lib/modules/manager/bazel-module/parser/index.ts index b53bce2241..cd25ead145 100644 --- a/lib/modules/manager/bazel-module/parser/index.ts +++ b/lib/modules/manager/bazel-module/parser/index.ts @@ -1,11 +1,10 @@ import { lang, query as q } from 'good-enough-parser'; import { Ctx } from '../context'; -import type { RecordFragment } from '../fragments'; -import { mavenRules } from './maven'; -import { moduleRules } from './module'; -import { ociRules } from './oci'; +import type { ResultFragment } from '../fragments'; +import { extensionTags } from './extension-tags'; +import { rules } from './rules'; -const rule = q.alt<Ctx>(moduleRules, mavenRules, ociRules); +const rule = q.alt<Ctx>(rules, extensionTags); const query = q.tree<Ctx>({ type: 'root-tree', @@ -15,7 +14,7 @@ const query = q.tree<Ctx>({ const starlarkLang = lang.createLang('starlark'); -export function parse(input: string): RecordFragment[] { +export function parse(input: string): ResultFragment[] { const parsedResult = starlarkLang.query(input, query, new Ctx()); return parsedResult?.results ?? []; } diff --git a/lib/modules/manager/bazel-module/parser/maven.ts b/lib/modules/manager/bazel-module/parser/maven.ts index 56376b8a13..9b6bf797b7 100644 --- a/lib/modules/manager/bazel-module/parser/maven.ts +++ b/lib/modules/manager/bazel-module/parser/maven.ts @@ -1,26 +1,23 @@ -import { query as q } from 'good-enough-parser'; import { z } from 'zod'; -import { regEx } from '../../../../util/regex'; import { MavenDatasource } from '../../../datasource/maven'; import { id as versioning } from '../../../versioning/gradle'; import type { PackageDependency } from '../../types'; -import type { Ctx } from '../context'; import { - RecordFragmentSchema, + ExtensionTagFragmentSchema, StringArrayFragmentSchema, StringFragmentSchema, } from '../fragments'; -import { kvParams } from './common'; -const artifactMethod = 'artifact'; -const installMethod = 'install'; +const artifactTag = 'artifact'; +const installTag = 'install'; const commonDepType = 'maven_install'; -const mavenVariableRegex = regEx(/^maven.*/); -const bzlmodMavenMethods = [installMethod, artifactMethod]; -const methodRegex = regEx(`^${bzlmodMavenMethods.join('|')}$`); -function getParsedRuleByMethod(method: string): string { - return `maven_${method}`; +export const mavenExtensionPrefix = 'maven'; + +export const mavenExtensionTags = [artifactTag, installTag]; + +function depTypeByTag(tag: string): string { + return `maven_${tag}`; } const ArtifactSpec = z.object({ @@ -30,32 +27,30 @@ const ArtifactSpec = z.object({ }); type ArtifactSpec = z.infer<typeof ArtifactSpec>; -const MavenArtifactTarget = RecordFragmentSchema.extend({ +const MavenArtifactTarget = ExtensionTagFragmentSchema.extend({ + extension: z.literal(mavenExtensionPrefix), + tag: z.literal(artifactTag), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal(getParsedRuleByMethod(artifactMethod)), - }), artifact: StringFragmentSchema, group: StringFragmentSchema, version: StringFragmentSchema, }), }).transform( - ({ children: { rule, artifact, group, version } }): PackageDependency[] => [ + ({ children: { artifact, group, version } }): PackageDependency[] => [ { datasource: MavenDatasource.id, versioning, depName: `${group.value}:${artifact.value}`, currentValue: version.value, - depType: rule.value, + depType: depTypeByTag(artifactTag), }, ], ); -const MavenInstallTarget = RecordFragmentSchema.extend({ +const MavenInstallTarget = ExtensionTagFragmentSchema.extend({ + extension: z.literal(mavenExtensionPrefix), + tag: z.literal(installTag), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal(getParsedRuleByMethod(installMethod)), - }), artifacts: StringArrayFragmentSchema.transform((artifacts) => { const result: ArtifactSpec[] = []; for (const { value } of artifacts.items) { @@ -69,16 +64,15 @@ const MavenInstallTarget = RecordFragmentSchema.extend({ }), repositories: StringArrayFragmentSchema, }), -}).transform( - ({ children: { rule, artifacts, repositories } }): PackageDependency[] => - artifacts.map(({ group, artifact, version: currentValue }) => ({ - datasource: MavenDatasource.id, - versioning, - depName: `${group}:${artifact}`, - currentValue, - depType: rule.value, - registryUrls: repositories.items.map((i) => i.value), - })), +}).transform(({ children: { artifacts, repositories } }): PackageDependency[] => + artifacts.map(({ group, artifact, version: currentValue }) => ({ + datasource: MavenDatasource.id, + versioning, + depName: `${group}:${artifact}`, + currentValue, + depType: depTypeByTag(installTag), + registryUrls: repositories.items.map((i) => i.value), + })), ); export const RuleToMavenPackageDep = z.union([ @@ -95,12 +89,12 @@ export function fillRegistryUrls( // registry urls are specified only in maven.install, not in maven.artifact packageDeps.flat().forEach((dep) => { - if (dep.depType === getParsedRuleByMethod(installMethod)) { + if (dep.depType === depTypeByTag(installTag)) { if (Array.isArray(dep.registryUrls)) { registryUrls.push(...dep.registryUrls); result.push(dep); } - } else if (dep.depType === getParsedRuleByMethod(artifactMethod)) { + } else if (dep.depType === depTypeByTag(artifactTag)) { artifactRules.push(dep); } }); @@ -115,24 +109,3 @@ export function fillRegistryUrls( return result; } - -export const mavenRules = q - .sym<Ctx>(mavenVariableRegex, (ctx, token) => { - return ctx.startRule(token.value); - }) - .op('.') - .sym(methodRegex, (ctx, token) => { - const rule = ctx.currentRecord.children.rule; - if (rule.type === 'string') { - rule.value = getParsedRuleByMethod(token.value); - } - return ctx; - }) - .join( - q.tree({ - type: 'wrapped-tree', - maxDepth: 1, - search: kvParams, - postHandler: (ctx) => ctx.endRule(), - }), - ); diff --git a/lib/modules/manager/bazel-module/parser/oci.ts b/lib/modules/manager/bazel-module/parser/oci.ts index 60b4e556a2..f295a987af 100644 --- a/lib/modules/manager/bazel-module/parser/oci.ts +++ b/lib/modules/manager/bazel-module/parser/oci.ts @@ -1,41 +1,30 @@ -import { query as q } from 'good-enough-parser'; import { z } from 'zod'; import { DockerDatasource } from '../../../datasource/docker'; import type { PackageDependency } from '../../types'; -import type { Ctx } from '../context'; -import { RecordFragmentSchema, StringFragmentSchema } from '../fragments'; -import { kvParams } from './common'; +import { ExtensionTagFragmentSchema, StringFragmentSchema } from '../fragments'; -export const RuleToDockerPackageDep = RecordFragmentSchema.extend({ +export const ociExtensionPrefix = 'oci'; + +const pullTag = 'pull'; + +export const ociExtensionTags = ['pull']; + +export const RuleToDockerPackageDep = ExtensionTagFragmentSchema.extend({ + extension: z.literal(ociExtensionPrefix), + tag: z.literal(pullTag), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('oci_pull'), - }), name: StringFragmentSchema, image: StringFragmentSchema, tag: StringFragmentSchema.optional(), digest: StringFragmentSchema.optional(), }), }).transform( - ({ children: { rule, name, image, tag, digest } }): PackageDependency => ({ + ({ children: { name, image, tag, digest } }): PackageDependency => ({ datasource: DockerDatasource.id, - depType: rule.value, + depType: 'oci_pull', depName: name.value, packageName: image.value, currentValue: tag?.value, currentDigest: digest?.value, }), ); - -export const ociRules = q - .sym<Ctx>('oci') - .op('.') - .sym('pull', (ctx, token) => ctx.startRule('oci_pull')) - .join( - q.tree({ - type: 'wrapped-tree', - maxDepth: 1, - search: kvParams, - postHandler: (ctx) => ctx.endRule(), - }), - ); diff --git a/lib/modules/manager/bazel-module/parser/module.ts b/lib/modules/manager/bazel-module/parser/rules.ts similarity index 57% rename from lib/modules/manager/bazel-module/parser/module.ts rename to lib/modules/manager/bazel-module/parser/rules.ts index 54371e9a7b..dfcadaf5db 100644 --- a/lib/modules/manager/bazel-module/parser/module.ts +++ b/lib/modules/manager/bazel-module/parser/rules.ts @@ -3,6 +3,16 @@ import { regEx } from '../../../../util/regex'; import type { Ctx } from '../context'; import { kvParams } from './common'; +// For the purpose of parsing bazel module files in Renovate, we consider a rule +// to be any "direct function application". For example: +// +// bazel_dep(name = "platforms", version = "0.0.11") +// ^^^^^^^^^ --> the "rule" +// +// In bazel, rules have typically a more narrow definition. However: +// - They are syntactically indistinguishable from, say, macros. +// - In informal speech, "rule" is often used as umbrella term. + const supportedRules = [ 'archive_override', 'bazel_dep', @@ -13,7 +23,7 @@ const supportedRules = [ ]; const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); -export const moduleRules = q +export const rules = q .sym<Ctx>(supportedRulesRegex, (ctx, token) => ctx.startRule(token.value)) .join( q.tree({ diff --git a/lib/modules/manager/bazel-module/rules.spec.ts b/lib/modules/manager/bazel-module/rules.spec.ts index cd035d3a86..53d67a7f5e 100644 --- a/lib/modules/manager/bazel-module/rules.spec.ts +++ b/lib/modules/manager/bazel-module/rules.spec.ts @@ -96,48 +96,43 @@ const gitRepositoryForUnsupportedPkgDep: BasePackageDep = { describe('modules/manager/bazel-module/rules', () => { describe('RuleToBazelModulePackageDep', () => { - const bazelDepWithoutDevDep = fragments.record({ - rule: fragments.string('bazel_dep'), + const bazelDepWithoutDevDep = fragments.rule('bazel_dep', { name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), }); - const bazelDepWithoutDevDepNoVersion = fragments.record({ - rule: fragments.string('bazel_dep'), + const bazelDepWithoutDevDepNoVersion = fragments.rule('bazel_dep', { name: fragments.string('rules_foo'), }); - const gitOverrideWithGihubHost = fragments.record({ - rule: fragments.string('git_override'), + const gitOverrideWithGihubHost = fragments.rule('git_override', { module_name: fragments.string('rules_foo'), remote: fragments.string('https://github.com/example/rules_foo.git'), commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'), }); - const gitOverrideWithUnsupportedHost = fragments.record({ - rule: fragments.string('git_override'), + const gitOverrideWithUnsupportedHost = fragments.rule('git_override', { module_name: fragments.string('rules_foo'), remote: fragments.string('https://nobuenos.com/example/rules_foo.git'), commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'), }); - const archiveOverride = fragments.record({ - rule: fragments.string('archive_override'), + const archiveOverride = fragments.rule('archive_override', { module_name: fragments.string('rules_foo'), urls: fragments.string('https://example.com/rules_foo.tar.gz'), }); - const localPathOverride = fragments.record({ - rule: fragments.string('local_path_override'), + const localPathOverride = fragments.rule('local_path_override', { module_name: fragments.string('rules_foo'), path: fragments.string('/path/to/module'), }); - const singleVersionOverride = fragments.record({ - rule: fragments.string('single_version_override'), + const singleVersionOverride = fragments.rule('single_version_override', { module_name: fragments.string('rules_foo'), version: fragments.string('1.2.3'), registry: fragments.string(customRegistryUrl), }); - const singleVersionOverrideWithRegistry = fragments.record({ - rule: fragments.string('single_version_override'), - module_name: fragments.string('rules_foo'), - registry: fragments.string(customRegistryUrl), - }); + const singleVersionOverrideWithRegistry = fragments.rule( + 'single_version_override', + { + module_name: fragments.string('rules_foo'), + registry: fragments.string(customRegistryUrl), + }, + ); it.each` msg | a | exp @@ -156,14 +151,12 @@ describe('modules/manager/bazel-module/rules', () => { }); describe('GitRepositoryToPackageDep', () => { - const gitRepositoryWithGihubHost = fragments.record({ - rule: fragments.string('git_repository'), + const gitRepositoryWithGihubHost = fragments.rule('git_repository', { name: fragments.string('rules_foo'), remote: fragments.string('https://github.com/example/rules_foo.git'), commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'), }); - const gitRepositoryWithUnsupportedHost = fragments.record({ - rule: fragments.string('git_repository'), + const gitRepositoryWithUnsupportedHost = fragments.rule('git_repository', { name: fragments.string('rules_foo'), remote: fragments.string('https://nobuenos.com/example/rules_foo.git'), commit: fragments.string('850cb49c8649e463b80ef7984e7c744279746170'), diff --git a/lib/modules/manager/bazel-module/rules.ts b/lib/modules/manager/bazel-module/rules.ts index ef0fe52fc6..5c2b133ebb 100644 --- a/lib/modules/manager/bazel-module/rules.ts +++ b/lib/modules/manager/bazel-module/rules.ts @@ -8,7 +8,7 @@ import { regEx } from '../../../util/regex'; import { BazelDatasource } from '../../datasource/bazel'; import { GithubTagsDatasource } from '../../datasource/github-tags'; import type { PackageDependency } from '../types'; -import { RecordFragmentSchema, StringFragmentSchema } from './fragments'; +import { RuleFragmentSchema, StringFragmentSchema } from './fragments'; // Rule Schemas @@ -63,39 +63,36 @@ export function bazelModulePackageDepToPackageDependency( return copy; } -const BazelDepToPackageDep = RecordFragmentSchema.extend({ +const BazelDepToPackageDep = RuleFragmentSchema.extend({ + rule: z.literal('bazel_dep'), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('bazel_dep'), - }), name: StringFragmentSchema, version: StringFragmentSchema.optional(), }), }).transform( - ({ children: { rule, name, version } }): BasePackageDep => ({ + ({ rule, children: { name, version } }): BasePackageDep => ({ datasource: BazelDatasource.id, - depType: rule.value, + depType: rule, depName: name.value, currentValue: version?.value, ...(version ? {} : { skipReason: 'unspecified-version' }), }), ); -const GitOverrideToPackageDep = RecordFragmentSchema.extend({ +const GitOverrideToPackageDep = RuleFragmentSchema.extend({ + rule: z.literal('git_override'), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('git_override'), - }), module_name: StringFragmentSchema, remote: StringFragmentSchema, commit: StringFragmentSchema, }), }).transform( ({ - children: { rule, module_name: moduleName, remote, commit }, + rule, + children: { module_name: moduleName, remote, commit }, }): OverridePackageDep => { const override: OverridePackageDep = { - depType: rule.value, + depType: rule, depName: moduleName.value, bazelDepSkipReason: 'git-dependency', currentDigest: commit.value, @@ -111,21 +108,20 @@ const GitOverrideToPackageDep = RecordFragmentSchema.extend({ }, ); -const SingleVersionOverrideToPackageDep = RecordFragmentSchema.extend({ +const SingleVersionOverrideToPackageDep = RuleFragmentSchema.extend({ + rule: z.literal('single_version_override'), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('single_version_override'), - }), module_name: StringFragmentSchema, version: StringFragmentSchema.optional(), registry: StringFragmentSchema.optional(), }), }).transform( ({ - children: { rule, module_name: moduleName, version, registry }, + rule, + children: { module_name: moduleName, version, registry }, }): BasePackageDep => { const base: BasePackageDep = { - depType: rule.value, + depType: rule, depName: moduleName.value, skipReason: 'ignored', }; @@ -145,17 +141,15 @@ const SingleVersionOverrideToPackageDep = RecordFragmentSchema.extend({ }, ); -const UnsupportedOverrideToPackageDep = RecordFragmentSchema.extend({ +const UnsupportedOverrideToPackageDep = RuleFragmentSchema.extend({ + rule: z.enum(['archive_override', 'local_path_override']), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.enum(['archive_override', 'local_path_override']), - }), module_name: StringFragmentSchema, }), }).transform( - ({ children: { rule, module_name: moduleName } }): OverridePackageDep => { + ({ rule, children: { module_name: moduleName } }): OverridePackageDep => { let bazelDepSkipReason: SkipReason = 'unsupported'; - switch (rule.value) { + switch (rule) { case 'archive_override': bazelDepSkipReason = 'file-dependency'; break; @@ -164,7 +158,7 @@ const UnsupportedOverrideToPackageDep = RecordFragmentSchema.extend({ break; } return { - depType: rule.value, + depType: rule, depName: moduleName.value, skipReason: 'unsupported-datasource', bazelDepSkipReason, @@ -244,18 +238,16 @@ export function toPackageDependencies( return collectByModule(packageDeps).map(processModulePkgDeps).flat(); } -export const GitRepositoryToPackageDep = RecordFragmentSchema.extend({ +export const GitRepositoryToPackageDep = RuleFragmentSchema.extend({ + rule: z.literal('git_repository'), children: z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('git_repository'), - }), name: StringFragmentSchema, remote: StringFragmentSchema, commit: StringFragmentSchema, }), -}).transform(({ children: { rule, name, remote, commit } }): BasePackageDep => { +}).transform(({ rule, children: { name, remote, commit } }): BasePackageDep => { const gitRepo: BasePackageDep = { - depType: rule.value, + depType: rule, depName: name.value, currentDigest: commit.value, }; -- GitLab