From 5c9822a2e08a96042b82ae38813cc87c505a980b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 20 Apr 2023 12:00:53 -0600 Subject: [PATCH 01/80] Initial manager files --- lib/modules/manager/api.ts | 2 + lib/modules/manager/bazel-module/extract.ts | 42 +++++++++++++++++++++ lib/modules/manager/bazel-module/index.ts | 10 +++++ 3 files changed, 54 insertions(+) create mode 100644 lib/modules/manager/bazel-module/extract.ts create mode 100644 lib/modules/manager/bazel-module/index.ts diff --git a/lib/modules/manager/api.ts b/lib/modules/manager/api.ts index 41dc108d9d1d8c..d6864da7482132 100644 --- a/lib/modules/manager/api.ts +++ b/lib/modules/manager/api.ts @@ -6,6 +6,7 @@ import * as azurePipelines from './azure-pipelines'; import * as batect from './batect'; import * as batectWrapper from './batect-wrapper'; import * as bazel from './bazel'; +import * as bazelModule from './bazel-module'; import * as bazelisk from './bazelisk'; import * as bicep from './bicep'; import * as bitbucketPipelines from './bitbucket-pipelines'; @@ -94,6 +95,7 @@ api.set('azure-pipelines', azurePipelines); api.set('batect', batect); api.set('batect-wrapper', batectWrapper); api.set('bazel', bazel); +api.set('bazel-module', bazelModule); api.set('bazelisk', bazelisk); api.set('bicep', bicep); api.set('bitbucket-pipelines', bitbucketPipelines); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts new file mode 100644 index 00000000000000..dadcf7e621ad09 --- /dev/null +++ b/lib/modules/manager/bazel-module/extract.ts @@ -0,0 +1,42 @@ +import type { PackageDependency, PackageFileContent } from '../types'; + +// import { parse } from './parser'; +// import { extractDepsFromFragment } from './rules'; +// import type { RecordFragment } from './types'; + +export function extractPackageFile( + content: string, + packageFile: string +): PackageFileContent | null { + const deps: PackageDependency[] = []; + + // const fragments: RecordFragment[] | null = parse(content, packageFile); + // if (!fragments) { + // return null; + // } + + // for (let idx = 0; idx < fragments.length; idx += 1) { + // const fragment = fragments[idx]; + // for (const dep of extractDepsFromFragment(fragment)) { + // dep.managerData = { idx }; + + // // // Selectively provide `replaceString` in order + // // // to auto-replace functionality work correctly. + // // const replaceString = fragment.value; + // // if ( + // // replaceString.startsWith('container_pull') || + // // replaceString.startsWith('oci_pull') || + // // replaceString.startsWith('git_repository') || + // // replaceString.startsWith('go_repository') + // // ) { + // // if (dep.currentValue && dep.currentDigest) { + // // dep.replaceString = replaceString; + // // } + // // } + + // deps.push(dep); + // } + // } + + return deps.length ? { deps } : null; +} diff --git a/lib/modules/manager/bazel-module/index.ts b/lib/modules/manager/bazel-module/index.ts new file mode 100644 index 00000000000000..679a861e70528f --- /dev/null +++ b/lib/modules/manager/bazel-module/index.ts @@ -0,0 +1,10 @@ +import { BazelRegistryDatasource } from '../../datasource/bazel-registry'; +import { extractPackageFile } from './extract'; + +export { extractPackageFile }; + +export const defaultConfig = { + fileMatch: ['(^|/)MODULE\\.bazel$'], +}; + +export const supportedDatasources = [BazelRegistryDatasource.id]; From 11ef4b1da94f3fc390942df44028f5695b760ced Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 21 Apr 2023 10:00:57 -0600 Subject: [PATCH 02/80] Add placeholder spec file --- lib/modules/manager/bazel-module/extract.spec.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 lib/modules/manager/bazel-module/extract.spec.ts diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts new file mode 100644 index 00000000000000..318508d3d7eb26 --- /dev/null +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -0,0 +1,3 @@ +describe('modules/manager/bazel-module/extract', () => { + it.todo('IMPLEMENT TESTS!'); +}); From a8d837497cd4cdf173d7e9d43b4de98ddb0ccf7d Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 21 Apr 2023 17:14:32 -0600 Subject: [PATCH 03/80] Save work --- .../manager/bazel-module/extract.spec.ts | 9 ++- lib/modules/manager/bazel-module/extract.ts | 10 +-- lib/modules/manager/bazel-module/parser.ts | 72 +++++++++++++++++++ .../manager/bazel-module/rules/bazel-dep.ts | 20 ++++++ .../manager/bazel-module/rules/index.spec.ts | 16 +++++ .../manager/bazel-module/rules/index.ts | 37 ++++++++++ .../manager/bazel-module/types.spec.ts | 14 ++++ lib/modules/manager/bazel-module/types.ts | 14 ++++ 8 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 lib/modules/manager/bazel-module/parser.ts create mode 100644 lib/modules/manager/bazel-module/rules/bazel-dep.ts create mode 100644 lib/modules/manager/bazel-module/rules/index.spec.ts create mode 100644 lib/modules/manager/bazel-module/rules/index.ts create mode 100644 lib/modules/manager/bazel-module/types.spec.ts create mode 100644 lib/modules/manager/bazel-module/types.ts diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 318508d3d7eb26..93465dd28d963a 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -1,3 +1,10 @@ +import { extractPackageFile } from '.'; + describe('modules/manager/bazel-module/extract', () => { - it.todo('IMPLEMENT TESTS!'); + describe('extractPackageFile()', () => { + it('returns empty if fails to parse', () => { + const res = extractPackageFile('blahhhhh:foo:@what\n', 'MODULE.bazel'); + expect(res).toBeNull(); + }); + }); }); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index dadcf7e621ad09..6b67c5f37b8a6a 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,3 +1,5 @@ +import { parse } from '../bazel/parser'; +import type { RecordFragment } from '../bazel/types'; import type { PackageDependency, PackageFileContent } from '../types'; // import { parse } from './parser'; @@ -10,10 +12,10 @@ export function extractPackageFile( ): PackageFileContent | null { const deps: PackageDependency[] = []; - // const fragments: RecordFragment[] | null = parse(content, packageFile); - // if (!fragments) { - // return null; - // } + const fragments: RecordFragment[] | null = parse(content, packageFile); + if (!fragments) { + return null; + } // for (let idx = 0; idx < fragments.length; idx += 1) { // const fragment = fragments[idx]; diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts new file mode 100644 index 00000000000000..c1d8c56be83ee4 --- /dev/null +++ b/lib/modules/manager/bazel-module/parser.ts @@ -0,0 +1,72 @@ +// // import { lang, lexer, parser, query as q } from 'good-enough-parser'; +// import { logger } from '../../../logger'; +// import type { NestedFragment, RecordFragment } from '../bazel/types'; + +// class Ctx { +// results: RecordFragment[] = []; +// stack: NestedFragment[] = []; +// // recordKey?: string; +// // subRecordKey?: string; +// // argIndex?: number; + +// constructor(readonly source: string) {} + +// // get currentFragment(): NestedFragment { +// // if (!this.stack) { +// // return +// // } +// // } +// } + +// // function currentFragment(ctx: Ctx): NestedFragment { +// // const deepestFragment = ctx.stack[ctx.stack.length - 1]; +// // return deepestFragment; +// // } + +// // function ruleNameHandler(ctx: Ctx, { value, offset }: lexer.Token): Ctx { +// // const ruleFragment = currentFragment(ctx); +// // if (ruleFragment.type === 'record') { +// // ruleFragment.children['rule'] = { type: 'string', value, offset }; +// // } +// // return ctx; +// // } + +// const regularRule = q +// .sym(supportedRulesRegex, (ctx, token) => +// ruleNameHandler(recordStartHandler(ctx, token), token) +// ) +// .join(ruleCall(kwParams)); + +// const rule = q.alt(regularRule); + +// const query = q.tree({ +// type: 'root-tree', +// maxDepth: 16, +// search: rule, +// }); + +// const starlark = lang.createLang('starlark'); + +// export function parse( +// input: string, +// packageFile?: string +// ): RecordFragment[] | null { +// // TODO: Add the mem cache. + +// let result: RecordFragment[] | null = null; +// try { +// const parsedResult = starlark.query(input, query, emptyCtx(input)); +// if (parsedResult) { +// result = parsedResult.results; +// } +// } catch (err) /* istanbul ignore next */ { +// logger.debug({ err, packageFile }, 'Bazel parsing error'); +// } + +// return result; +// } + +// // export class Parser { +// // static readonly starlark = lang.createLang('starlark'); +// // constructor(readonly input: string, readonly packageFile?: string) {} +// // } diff --git a/lib/modules/manager/bazel-module/rules/bazel-dep.ts b/lib/modules/manager/bazel-module/rules/bazel-dep.ts new file mode 100644 index 00000000000000..3d548cbfc0f962 --- /dev/null +++ b/lib/modules/manager/bazel-module/rules/bazel-dep.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; +import { BazelRegistryDatasource } from '../../../datasource/bazel-registry'; +import type { PackageDependency } from '../../types'; + +export const BazelDepTarget = z + .object({ + rule: z.enum(['bazel_dep']), + name: z.string(), + version: z.string(), + dev_dependency: z.boolean().optional(), + }) + .transform(({ rule, name, version, dev_dependency }): PackageDependency[] => { + const dep: PackageDependency = { + datasource: BazelRegistryDatasource.id, + depType: rule, + depName: name, + currentValue: version, + }; + return [dep]; + }); diff --git a/lib/modules/manager/bazel-module/rules/index.spec.ts b/lib/modules/manager/bazel-module/rules/index.spec.ts new file mode 100644 index 00000000000000..21b8d655a13407 --- /dev/null +++ b/lib/modules/manager/bazel-module/rules/index.spec.ts @@ -0,0 +1,16 @@ +// import { extractDepsFromFragment } from '.'; + +describe('modules/manager/bazel-module/rules/index', () => { + it.todo('IMPLEMENT ME!'); + // it.each` + // a | exp + // ${'bazel_dep(name = "rules_foo", version = "1.2.3")'} | ${[{ rule: 'bazel_dep', name: 'rules_foo', version: '1.2.3' }]} + // `('extractDepsFromFragmentData($a)', ({ a, exp }) => { + // const fragments = parse(a, 'MODULE.bazel'); + // if (!fragments) { + // throw new Error('Expected fragments not to be null.'); + // } + // expect(fragments).toHaveLength(1); + // expect(extractDepsFromFragment(fragments[0])).toEqual(exp); + // }); +}); diff --git a/lib/modules/manager/bazel-module/rules/index.ts b/lib/modules/manager/bazel-module/rules/index.ts new file mode 100644 index 00000000000000..a33d1f79ca44fe --- /dev/null +++ b/lib/modules/manager/bazel-module/rules/index.ts @@ -0,0 +1,37 @@ +import type { Fragment, FragmentData } from '../../bazel/types'; +import type { PackageDependency } from '../../types'; +import { BazelDepTarget } from './bazel-dep'; + +export function extractDepsFromFragment( + fragment: Fragment +): PackageDependency[] { + const fragmentData = extract(fragment); + return extractDepsFromFragmentData(fragmentData); +} + +function extract(fragment: Fragment): FragmentData { + if (fragment.type === 'string') { + return fragment.value; + } + + if (fragment.type === 'record') { + const { children } = fragment; + const result: Record = {}; + for (const [key, value] of Object.entries(children)) { + result[key] = extract(value); + } + return result; + } + + return fragment.children.map(extract); +} + +function extractDepsFromFragmentData( + fragmentData: FragmentData +): PackageDependency[] { + const res = BazelDepTarget.safeParse(fragmentData); + if (!res.success) { + return []; + } + return res.data; +} diff --git a/lib/modules/manager/bazel-module/types.spec.ts b/lib/modules/manager/bazel-module/types.spec.ts new file mode 100644 index 00000000000000..d039340622b463 --- /dev/null +++ b/lib/modules/manager/bazel-module/types.spec.ts @@ -0,0 +1,14 @@ +import { Stack } from './types'; + +describe('modules/manager/bazel-module/types', () => { + describe('Stack', () => { + it.each` + items | exp + ${[]} | ${null} + ${['first', 'second']} | ${'second'} + `('get current for $items', ({ items, exp }) => { + const stack = Stack.create(...items); + expect(stack.current).toBe(exp); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/types.ts b/lib/modules/manager/bazel-module/types.ts new file mode 100644 index 00000000000000..433ae7e7cf2d84 --- /dev/null +++ b/lib/modules/manager/bazel-module/types.ts @@ -0,0 +1,14 @@ +export class Stack extends Array { + static create(...items: Array): Stack { + const stack = new Stack(); + stack.push(...items); + return stack; + } + + get current(): T | null { + if (!this.length) { + return null; + } + return this[this.length - 1]; + } +} From b1b7c2ef51e11246544a2197d18857db4417bd1c Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 25 Apr 2023 11:45:21 -0600 Subject: [PATCH 04/80] Use new datasource name --- lib/modules/manager/bazel-module/index.ts | 4 ++-- .../manager/bazel-module/rules/bazel-dep.ts | 24 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/modules/manager/bazel-module/index.ts b/lib/modules/manager/bazel-module/index.ts index 679a861e70528f..c1b65aab70180f 100644 --- a/lib/modules/manager/bazel-module/index.ts +++ b/lib/modules/manager/bazel-module/index.ts @@ -1,4 +1,4 @@ -import { BazelRegistryDatasource } from '../../datasource/bazel-registry'; +import { BazelDatasource } from '../../datasource/bazel'; import { extractPackageFile } from './extract'; export { extractPackageFile }; @@ -7,4 +7,4 @@ export const defaultConfig = { fileMatch: ['(^|/)MODULE\\.bazel$'], }; -export const supportedDatasources = [BazelRegistryDatasource.id]; +export const supportedDatasources = [BazelDatasource.id]; diff --git a/lib/modules/manager/bazel-module/rules/bazel-dep.ts b/lib/modules/manager/bazel-module/rules/bazel-dep.ts index 3d548cbfc0f962..c529917012529e 100644 --- a/lib/modules/manager/bazel-module/rules/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/rules/bazel-dep.ts @@ -1,20 +1,22 @@ import { z } from 'zod'; -import { BazelRegistryDatasource } from '../../../datasource/bazel-registry'; +import { BazelDatasource } from '../../../datasource/bazel'; import type { PackageDependency } from '../../types'; -export const BazelDepTarget = z - .object({ - rule: z.enum(['bazel_dep']), - name: z.string(), - version: z.string(), - dev_dependency: z.boolean().optional(), - }) - .transform(({ rule, name, version, dev_dependency }): PackageDependency[] => { +export const BazelDepTarget = z.object({ + rule: z.enum(['bazel_dep']), + name: z.string(), + version: z.string(), + dev_dependency: z.boolean().optional(), +}); + +export const BazelDepToPackageDependency = BazelDepTarget.transform( + ({ rule, name, version, dev_dependency }): PackageDependency[] => { const dep: PackageDependency = { - datasource: BazelRegistryDatasource.id, + datasource: BazelDatasource.id, depType: rule, depName: name, currentValue: version, }; return [dep]; - }); + } +); From bfde79f00300c995e5996f8d6c675703a1ce0807 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 25 Apr 2023 12:59:29 -0600 Subject: [PATCH 05/80] Have current return undefined instead of null --- lib/modules/manager/bazel-module/types.spec.ts | 2 +- lib/modules/manager/bazel-module/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/manager/bazel-module/types.spec.ts b/lib/modules/manager/bazel-module/types.spec.ts index d039340622b463..5b941ea0421b01 100644 --- a/lib/modules/manager/bazel-module/types.spec.ts +++ b/lib/modules/manager/bazel-module/types.spec.ts @@ -4,7 +4,7 @@ describe('modules/manager/bazel-module/types', () => { describe('Stack', () => { it.each` items | exp - ${[]} | ${null} + ${[]} | ${undefined} ${['first', 'second']} | ${'second'} `('get current for $items', ({ items, exp }) => { const stack = Stack.create(...items); diff --git a/lib/modules/manager/bazel-module/types.ts b/lib/modules/manager/bazel-module/types.ts index 433ae7e7cf2d84..fa07e58d8c187c 100644 --- a/lib/modules/manager/bazel-module/types.ts +++ b/lib/modules/manager/bazel-module/types.ts @@ -5,9 +5,9 @@ export class Stack extends Array { return stack; } - get current(): T | null { + get current(): T | undefined { if (!this.length) { - return null; + return undefined; } return this[this.length - 1]; } From d521f727ad8facec701e601e1b356a5dd46f6db2 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 12:39:38 -0600 Subject: [PATCH 06/80] Initial parse with type coercion. --- .../manager/bazel-module/parser.spec.ts | 41 ++++ lib/modules/manager/bazel-module/parser.ts | 228 +++++++++++++----- .../manager/bazel-module/rules/index.ts | 70 +++--- .../manager/bazel-module/types.spec.ts | 2 +- lib/modules/manager/bazel-module/types.ts | 132 +++++++++- 5 files changed, 374 insertions(+), 99 deletions(-) create mode 100644 lib/modules/manager/bazel-module/parser.spec.ts diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts new file mode 100644 index 00000000000000..3bce61771af05c --- /dev/null +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -0,0 +1,41 @@ +import { codeBlock } from 'common-tags'; +import { Ctx, parse } from './parser'; +import { RecordFragment, StringFragment } from './types'; + +describe('modules/manager/bazel-module/parser', () => { + describe('Ctx', () => { + it('construct bazel_dep', () => { + const ctx = new Ctx() + .startRule('bazel_dep') + .startAttribute('name') + .setAttributeValue('rules_foo') + .startAttribute('version') + .setAttributeValue('1.2.3') + .endRule(); + + expect(ctx.results).toEqual([ + new RecordFragment({ + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + }), + ]); + }); + }); + + describe('parse', () => { + it('finds simple bazel_dep', () => { + const input = codeBlock` + bazel_dep(name = "rules_foo", version = "1.2.3") + `; + const res = parse(input, 'MODULE.bazel'); + expect(res).toEqual([ + new RecordFragment({ + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + }), + ]); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index c1d8c56be83ee4..c0b50a401e2706 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -1,70 +1,168 @@ -// // import { lang, lexer, parser, query as q } from 'good-enough-parser'; -// import { logger } from '../../../logger'; -// import type { NestedFragment, RecordFragment } from '../bazel/types'; - -// class Ctx { -// results: RecordFragment[] = []; -// stack: NestedFragment[] = []; -// // recordKey?: string; -// // subRecordKey?: string; -// // argIndex?: number; - -// constructor(readonly source: string) {} - -// // get currentFragment(): NestedFragment { -// // if (!this.stack) { -// // return -// // } -// // } -// } - -// // function currentFragment(ctx: Ctx): NestedFragment { -// // const deepestFragment = ctx.stack[ctx.stack.length - 1]; -// // return deepestFragment; -// // } +import { lang, query as q } from 'good-enough-parser'; +import { logger } from '../../../logger'; +import { supportedRulesRegex } from './rules'; +import { + AttributeFragment, + ChildFragments, + Fragment, + Fragments, + RecordFragment, + Stack, + StringFragment, +} from './types'; -// // function ruleNameHandler(ctx: Ctx, { value, offset }: lexer.Token): Ctx { -// // const ruleFragment = currentFragment(ctx); -// // if (ruleFragment.type === 'record') { -// // ruleFragment.children['rule'] = { type: 'string', value, offset }; -// // } -// // return ctx; -// // } +// Represents the fields that the context must have. +export interface CtxCompatible { + results: RecordFragment[]; + stack: Stack; +} + +export class Ctx implements CtxCompatible { + results: RecordFragment[] = []; + stack = Stack.create(); + + // This exists because the good-enough-parser gives a cloned instance of our + // Ctx instance. This instance is missing the Ctx prototype. + static from(obj: CtxCompatible): Ctx { + Object.setPrototypeOf(obj, Ctx.prototype); + const ctx = obj as Ctx; + const stackItems = ctx.stack.map((item) => Fragments.asFragment(item)); + ctx.stack = Stack.create(...stackItems); + ctx.results = ctx.results.map((item) => Fragments.asRecord(item)); + return ctx; + } + + private newError(msg: string): Error { + return new Error(`${msg} ctx: ${JSON.stringify(this)}`); + } + + get currentRecord(): RecordFragment { + const current = this.stack.current; + if (current instanceof RecordFragment) { + return current; + } + throw this.newError('Requested current record, but does not exist.'); + } + + get currentAttribute(): AttributeFragment { + const current = this.stack.current; + if (current instanceof AttributeFragment) { + return current; + } + throw this.newError('Requested current attribute, but does not exist.'); + } + + startRecord(children: ChildFragments): Ctx { + const record = new RecordFragment(children); + this.stack.push(record); + return this; + } + + endRecord(): Ctx { + const record = this.currentRecord; + this.results.push(record); + this.stack.pop(); + return this; + } + + startRule(name: string): Ctx { + return this.startRecord({ rule: new StringFragment(name) }); + } + + endRule(): Ctx { + return this.endRecord(); + } + + startAttribute(name: string): Ctx { + this.stack.push(new AttributeFragment(name)); + return this; + } + + setAttributeValue(value: string): Ctx { + const attrib = this.currentAttribute; + attrib.value = new StringFragment(value); + return this.endAttribute(); + } + + endAttribute(): Ctx { + const attrib = this.currentAttribute; + if (!attrib.value) { + throw this.newError(`No value was set for the attribute. ${attrib.name}`); + } + this.stack.pop(); + const record = this.currentRecord; + record.children[attrib.name] = attrib.value; + return this; + } +} + +/** + * Matches key-value pairs: + * - `tag = "1.2.3"` + * - `name = "foobar"` + * - `deps = ["foo", "bar"]` + * - ` + * artifacts = [ + maven.artifact( + group = "com.example1", + artifact = "foobar", + version = "1.2.3", + ) + ] + ` + **/ +const kwParams = q + .sym((ctx, token) => { + return Ctx.from(ctx).startAttribute(token.value); + }) + .op('=') + .str((ctx, token) => { + return Ctx.from(ctx).setAttributeValue(token.value); + }); + +const moduleRules = q + .sym(supportedRulesRegex, (ctx, token) => { + return Ctx.from(ctx).startRule(token.value); + }) + .join( + q.tree({ + type: 'wrapped-tree', + maxDepth: 1, + search: kwParams, + postHandler: (ctx, tree) => { + return Ctx.from(ctx).endRule(); + }, + }) + ); + +const rule = q.alt(moduleRules); + +const query = q.tree({ + type: 'root-tree', + maxDepth: 16, + search: rule, +}); + +const starlark = lang.createLang('starlark'); + +export function parse( + input: string, + packageFile?: string +): RecordFragment[] | null { + // TODO: Add the mem cache. + + let result: RecordFragment[] | null = null; + try { + const parsedResult = starlark.query(input, query, new Ctx()); + if (parsedResult) { + result = parsedResult.results; + } + } catch (err) /* istanbul ignore next */ { + logger.debug({ err, packageFile }, 'Bazel parsing error'); + } -// const regularRule = q -// .sym(supportedRulesRegex, (ctx, token) => -// ruleNameHandler(recordStartHandler(ctx, token), token) -// ) -// .join(ruleCall(kwParams)); - -// const rule = q.alt(regularRule); - -// const query = q.tree({ -// type: 'root-tree', -// maxDepth: 16, -// search: rule, -// }); - -// const starlark = lang.createLang('starlark'); - -// export function parse( -// input: string, -// packageFile?: string -// ): RecordFragment[] | null { -// // TODO: Add the mem cache. - -// let result: RecordFragment[] | null = null; -// try { -// const parsedResult = starlark.query(input, query, emptyCtx(input)); -// if (parsedResult) { -// result = parsedResult.results; -// } -// } catch (err) /* istanbul ignore next */ { -// logger.debug({ err, packageFile }, 'Bazel parsing error'); -// } - -// return result; -// } + return result; +} // // export class Parser { // // static readonly starlark = lang.createLang('starlark'); diff --git a/lib/modules/manager/bazel-module/rules/index.ts b/lib/modules/manager/bazel-module/rules/index.ts index a33d1f79ca44fe..bfa8c3a1401e10 100644 --- a/lib/modules/manager/bazel-module/rules/index.ts +++ b/lib/modules/manager/bazel-module/rules/index.ts @@ -1,37 +1,43 @@ -import type { Fragment, FragmentData } from '../../bazel/types'; -import type { PackageDependency } from '../../types'; -import { BazelDepTarget } from './bazel-dep'; +import { regEx } from '../../../../util/regex'; -export function extractDepsFromFragment( - fragment: Fragment -): PackageDependency[] { - const fragmentData = extract(fragment); - return extractDepsFromFragmentData(fragmentData); -} +const supportedRules = ['bazel_dep']; +export const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); -function extract(fragment: Fragment): FragmentData { - if (fragment.type === 'string') { - return fragment.value; - } +// import type { Fragment, FragmentData } from '../../bazel/types'; +// import type { PackageDependency } from '../../types'; +// import { BazelDepTarget } from './bazel-dep'; - if (fragment.type === 'record') { - const { children } = fragment; - const result: Record = {}; - for (const [key, value] of Object.entries(children)) { - result[key] = extract(value); - } - return result; - } +// export function extractDepsFromFragment( +// fragment: Fragment +// ): PackageDependency[] { +// const fragmentData = extract(fragment); +// return extractDepsFromFragmentData(fragmentData); +// } - return fragment.children.map(extract); -} +// function extract(fragment: Fragment): FragmentData { +// if (fragment.type === 'string') { +// return fragment.value; +// } -function extractDepsFromFragmentData( - fragmentData: FragmentData -): PackageDependency[] { - const res = BazelDepTarget.safeParse(fragmentData); - if (!res.success) { - return []; - } - return res.data; -} +// if (fragment.type === 'record') { +// const { children } = fragment; +// const result: Record = {}; +// for (const [key, value] of Object.entries(children)) { +// result[key] = extract(value); +// } +// return result; +// } + +// return fragment.children.map(extract); +// } + +// function extractDepsFromFragmentData( +// fragmentData: FragmentData +// ): PackageDependency[] { +// const res = BazelDepTarget.safeParse(fragmentData); +// if (!res.success) { +// return []; +// } +// return res.data; +// } +// diff --git a/lib/modules/manager/bazel-module/types.spec.ts b/lib/modules/manager/bazel-module/types.spec.ts index 5b941ea0421b01..73171c06572982 100644 --- a/lib/modules/manager/bazel-module/types.spec.ts +++ b/lib/modules/manager/bazel-module/types.spec.ts @@ -8,7 +8,7 @@ describe('modules/manager/bazel-module/types', () => { ${['first', 'second']} | ${'second'} `('get current for $items', ({ items, exp }) => { const stack = Stack.create(...items); - expect(stack.current).toBe(exp); + expect(stack.safeCurrent).toBe(exp); }); }); }); diff --git a/lib/modules/manager/bazel-module/types.ts b/lib/modules/manager/bazel-module/types.ts index fa07e58d8c187c..d885652098cd99 100644 --- a/lib/modules/manager/bazel-module/types.ts +++ b/lib/modules/manager/bazel-module/types.ts @@ -1,3 +1,5 @@ +// import type { lexer } from 'good-enough-parser'; + export class Stack extends Array { static create(...items: Array): Stack { const stack = new Stack(); @@ -5,10 +7,138 @@ export class Stack extends Array { return stack; } - get current(): T | undefined { + get safeCurrent(): T | undefined { if (!this.length) { return undefined; } return this[this.length - 1]; } + + get current(): T { + const c = this.safeCurrent; + if (!c) { + throw new Error('Requested current, but no value.'); + } + return c; + } +} + +export type FragmentType = 'string' | 'array' | 'record' | 'attribute'; + +export interface FragmentCompatible { + readonly type: FragmentType; +} + +export class StringFragment implements FragmentCompatible { + readonly type: FragmentType = 'string'; + constructor(readonly value: string) {} +} + +export class ArrayFragment implements FragmentCompatible { + readonly type: FragmentType = 'array'; + items: ValueFragment[] = []; +} + +export class RecordFragment implements FragmentCompatible { + readonly type: FragmentType = 'record'; + children: ChildFragments; + + constructor(children: ChildFragments = {}) { + this.children = children; + } +} + +export class AttributeFragment implements FragmentCompatible { + readonly type: FragmentType = 'attribute'; + value?: ValueFragment; + constructor(readonly name: string) {} +} + +export type ValueFragment = ArrayFragment | RecordFragment | StringFragment; +export type ChildFragments = Record; +export type Fragment = ValueFragment | AttributeFragment; + +export class Fragments { + private static typeError(expected: string, actual: string): Error { + return new Error(`Expected type ${expected}, but was ${actual}.`); + } + + private static checkType(expected: string, actual: string): void { + if (expected === actual) { + return; + } + throw Fragments.typeError(expected, actual); + } + + static safeAsValue(frag: FragmentCompatible): ValueFragment | undefined { + switch (frag.type) { + case 'string': + return Fragments.asString(frag); + break; + case 'array': + return Fragments.asArray(frag); + break; + case 'record': + return Fragments.asRecord(frag); + break; + default: + return undefined; + } + } + + static asValue(frag: FragmentCompatible): ValueFragment { + const value = Fragments.safeAsValue(frag); + if (value) { + return value; + } + throw new Error(`Unexpected fragment type. ${frag.type}`); + } + + static asFragment(frag: FragmentCompatible): Fragment { + const value = Fragments.safeAsValue(frag); + if (value) { + return value; + } + if (frag.type === 'attribute') { + return Fragments.asAttribute(frag); + } + throw new Error(`Unexpected fragment type. ${frag.type}`); + } + + static asString(frag: FragmentCompatible): StringFragment { + Fragments.checkType('string', frag.type); + Object.setPrototypeOf(frag, StringFragment.prototype); + return frag as StringFragment; + } + + static asArray(frag: FragmentCompatible): ArrayFragment { + Fragments.checkType('array', frag.type); + Object.setPrototypeOf(frag, ArrayFragment.prototype); + const array = frag as ArrayFragment; + for (let i = 0; i < array.items.length; i++) { + array.items[i] = Fragments.asValue(array.items[i]); + } + return array; + } + + static asRecord(frag: FragmentCompatible): RecordFragment { + Fragments.checkType('record', frag.type); + Object.setPrototypeOf(frag, RecordFragment.prototype); + const record = frag as RecordFragment; + for (const prop in record.children) { + const child = record.children[prop]; + record.children[prop] = Fragments.asValue(child); + } + return record; + } + + static asAttribute(frag: FragmentCompatible): AttributeFragment { + Fragments.checkType('attribute', frag.type); + Object.setPrototypeOf(frag, AttributeFragment.prototype); + const attribute = frag as AttributeFragment; + if (attribute.value) { + attribute.value = Fragments.asValue(attribute.value); + } + return attribute; + } } From 30688008865bd07e188687c0fab2c2a6e2eea99b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 16:23:38 -0600 Subject: [PATCH 07/80] Refactor to use processStack --- .../manager/bazel-module/parser.spec.ts | 59 +++++++--- lib/modules/manager/bazel-module/parser.ts | 106 ++++++++++++++---- lib/modules/manager/bazel-module/types.ts | 48 +++++++- 3 files changed, 175 insertions(+), 38 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index 3bce61771af05c..bdc5e652dfcf8f 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -1,6 +1,6 @@ import { codeBlock } from 'common-tags'; import { Ctx, parse } from './parser'; -import { RecordFragment, StringFragment } from './types'; +import { ArrayFragment, RecordFragment, StringFragment } from './types'; describe('modules/manager/bazel-module/parser', () => { describe('Ctx', () => { @@ -8,17 +8,47 @@ describe('modules/manager/bazel-module/parser', () => { const ctx = new Ctx() .startRule('bazel_dep') .startAttribute('name') - .setAttributeValue('rules_foo') + .addString('rules_foo') .startAttribute('version') - .setAttributeValue('1.2.3') + .addString('1.2.3') .endRule(); expect(ctx.results).toEqual([ - new RecordFragment({ - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), - }), + new RecordFragment( + { + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + }, + true + ), + ]); + }); + + it('construct a rule with array arg', () => { + const ctx = new Ctx() + .startRule('foo_library') + .startAttribute('name') + .addString('my_library') + .startAttribute('srcs') + .startArray() + .addArrayItem('first') + .addArrayItem('second') + .endArray() + .endRule(); + + expect(ctx.results).toEqual([ + new RecordFragment( + { + rule: new StringFragment('foo_library'), + name: new StringFragment('my_library'), + srcs: new ArrayFragment( + [new StringFragment('first'), new StringFragment('second')], + true + ), + }, + true + ), ]); }); }); @@ -30,11 +60,14 @@ describe('modules/manager/bazel-module/parser', () => { `; const res = parse(input, 'MODULE.bazel'); expect(res).toEqual([ - new RecordFragment({ - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), - }), + new RecordFragment( + { + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + }, + true + ), ]); }); }); diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index c0b50a401e2706..88d0e669821ca6 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -2,6 +2,7 @@ import { lang, query as q } from 'good-enough-parser'; import { logger } from '../../../logger'; import { supportedRulesRegex } from './rules'; import { + ArrayFragment, AttributeFragment, ChildFragments, Fragment, @@ -9,16 +10,17 @@ import { RecordFragment, Stack, StringFragment, + ValueFragment, } from './types'; // Represents the fields that the context must have. export interface CtxCompatible { - results: RecordFragment[]; + results: ValueFragment[]; stack: Stack; } export class Ctx implements CtxCompatible { - results: RecordFragment[] = []; + results: ValueFragment[] = []; stack = Stack.create(); // This exists because the good-enough-parser gives a cloned instance of our @@ -52,6 +54,58 @@ export class Ctx implements CtxCompatible { throw this.newError('Requested current attribute, but does not exist.'); } + get currentArray(): ArrayFragment { + const current = this.stack.current; + if (current instanceof ArrayFragment) { + return current; + } + throw this.newError('Requested current array, but does not exist.'); + } + + private popStack(): void { + const current = this.stack.pop(); + if (!current) { + return; + } + const value = Fragments.safeAsValue(current); + const attribute = Fragments.safeAsAttribute(current); + const newCurrent = this.stack.safeCurrent; + if (value) { + if (newCurrent && 'addValue' in newCurrent) { + newCurrent.addValue(value); + return; + } + if (!newCurrent) { + this.results.push(value); + return; + } + } else if (attribute) { + if (newCurrent && 'addAttribute' in newCurrent) { + newCurrent.addAttribute(attribute); + return; + } + if (!newCurrent) { + throw this.newError('Processing an attribute but there is no parent.'); + } + } else { + throw this.newError('We are in a bad place.'); + } + } + + private processStack(): Ctx { + let current = this.stack.safeCurrent; + while (current?.isComplete) { + this.popStack(); + current = this.stack.safeCurrent; + } + return this; + } + + addString(value: string): Ctx { + this.stack.push(new StringFragment(value)); + return this.processStack(); + } + startRecord(children: ChildFragments): Ctx { const record = new RecordFragment(children); this.stack.push(record); @@ -60,9 +114,8 @@ export class Ctx implements CtxCompatible { endRecord(): Ctx { const record = this.currentRecord; - this.results.push(record); - this.stack.pop(); - return this; + record.isComplete = true; + return this.processStack(); } startRule(name: string): Ctx { @@ -75,25 +128,36 @@ export class Ctx implements CtxCompatible { startAttribute(name: string): Ctx { this.stack.push(new AttributeFragment(name)); - return this; + return this.processStack(); } - setAttributeValue(value: string): Ctx { - const attrib = this.currentAttribute; - attrib.value = new StringFragment(value); - return this.endAttribute(); + // endAttribute(): Ctx { + // const attrib = this.currentAttribute; + // if (!attrib.value) { + // throw this.newError(`No value was set for the attribute. ${attrib.name}`); + // } + // this.stack.pop(); + // const record = this.currentRecord; + // record.children[attrib.name] = attrib.value; + // return this; + // } + + startArray(): Ctx { + this.stack.push(new ArrayFragment()); + return this.processStack(); } - endAttribute(): Ctx { - const attrib = this.currentAttribute; - if (!attrib.value) { - throw this.newError(`No value was set for the attribute. ${attrib.name}`); - } - this.stack.pop(); - const record = this.currentRecord; - record.children[attrib.name] = attrib.value; + addArrayItem(value: string): Ctx { + const array = this.currentArray; + array.items.push(new StringFragment(value)); return this; } + + endArray(): Ctx { + const array = this.currentArray; + array.isComplete = true; + return this.processStack(); + } } /** @@ -117,7 +181,7 @@ const kwParams = q }) .op('=') .str((ctx, token) => { - return Ctx.from(ctx).setAttributeValue(token.value); + return Ctx.from(ctx).addString(token.value); }); const moduleRules = q @@ -148,10 +212,10 @@ const starlark = lang.createLang('starlark'); export function parse( input: string, packageFile?: string -): RecordFragment[] | null { +): ValueFragment[] | null { // TODO: Add the mem cache. - let result: RecordFragment[] | null = null; + let result: ValueFragment[] | null = null; try { const parsedResult = starlark.query(input, query, new Ctx()); if (parsedResult) { diff --git a/lib/modules/manager/bazel-module/types.ts b/lib/modules/manager/bazel-module/types.ts index d885652098cd99..c957c5d137a18d 100644 --- a/lib/modules/manager/bazel-module/types.ts +++ b/lib/modules/manager/bazel-module/types.ts @@ -1,4 +1,4 @@ -// import type { lexer } from 'good-enough-parser'; +import is from '@sindresorhus/is'; export class Stack extends Array { static create(...items: Array): Stack { @@ -31,20 +31,40 @@ export interface FragmentCompatible { export class StringFragment implements FragmentCompatible { readonly type: FragmentType = 'string'; + readonly isComplete = true; constructor(readonly value: string) {} } export class ArrayFragment implements FragmentCompatible { readonly type: FragmentType = 'array'; + isComplete = false; items: ValueFragment[] = []; + + constructor(items: ValueFragment[] = [], isComplete = false) { + this.items = items; + this.isComplete = isComplete; + } + + addValue(item: ValueFragment): void { + this.items.push(item); + } } export class RecordFragment implements FragmentCompatible { readonly type: FragmentType = 'record'; + isComplete = false; children: ChildFragments; - constructor(children: ChildFragments = {}) { + constructor(children: ChildFragments = {}, isComplete = false) { this.children = children; + this.isComplete = isComplete; + } + + addAttribute(attrib: AttributeFragment): void { + if (!attrib.value) { + throw new Error('The attribute fragment does not have a value.'); + } + this.children[attrib.name] = attrib.value; } } @@ -52,6 +72,14 @@ export class AttributeFragment implements FragmentCompatible { readonly type: FragmentType = 'attribute'; value?: ValueFragment; constructor(readonly name: string) {} + + get isComplete(): boolean { + return is.truthy(this.value); + } + + addValue(item: ValueFragment): void { + this.value = item; + } } export type ValueFragment = ArrayFragment | RecordFragment | StringFragment; @@ -132,8 +160,12 @@ export class Fragments { return record; } - static asAttribute(frag: FragmentCompatible): AttributeFragment { - Fragments.checkType('attribute', frag.type); + static safeAsAttribute( + frag: FragmentCompatible + ): AttributeFragment | undefined { + if (frag.type !== 'attribute') { + return undefined; + } Object.setPrototypeOf(frag, AttributeFragment.prototype); const attribute = frag as AttributeFragment; if (attribute.value) { @@ -141,4 +173,12 @@ export class Fragments { } return attribute; } + + static asAttribute(frag: FragmentCompatible): AttributeFragment { + const attribute = Fragments.safeAsAttribute(frag); + if (attribute) { + return attribute; + } + throw this.typeError('attribute', frag.type); + } } From e76e6715f05d616fe10bf25e2a78f5567f150043 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 16:29:08 -0600 Subject: [PATCH 08/80] Add complex Ctx test --- lib/modules/manager/bazel-module/parser.spec.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index bdc5e652dfcf8f..f87a82d94ec28c 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -35,6 +35,9 @@ describe('modules/manager/bazel-module/parser', () => { .addArrayItem('first') .addArrayItem('second') .endArray() + .startAttribute('tags') + .startRule('get_tags') + .endRule() .endRule(); expect(ctx.results).toEqual([ @@ -46,6 +49,10 @@ describe('modules/manager/bazel-module/parser', () => { [new StringFragment('first'), new StringFragment('second')], true ), + tags: new RecordFragment( + { rule: new StringFragment('get_tags') }, + true + ), }, true ), From d5a59e2ac6a4bd630c29bde69664f58052a1c930 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 17:25:38 -0600 Subject: [PATCH 09/80] Add lifecycle tests for fragments --- .../manager/bazel-module/fragments.spec.ts | 65 +++++++ lib/modules/manager/bazel-module/fragments.ts | 166 ++++++++++++++++++ .../manager/bazel-module/parser.spec.ts | 2 +- lib/modules/manager/bazel-module/parser.ts | 6 +- lib/modules/manager/bazel-module/types.ts | 162 ----------------- 5 files changed, 235 insertions(+), 166 deletions(-) create mode 100644 lib/modules/manager/bazel-module/fragments.spec.ts create mode 100644 lib/modules/manager/bazel-module/fragments.ts diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts new file mode 100644 index 00000000000000..2ff9c9a23ff721 --- /dev/null +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -0,0 +1,65 @@ +import { + ArrayFragment, + AttributeFragment, + RecordFragment, + StringFragment, +} from './fragments'; + +describe('modules/manager/bazel-module/fragments', () => { + describe('StringFragment', () => { + it('constructor ', () => { + const string = new StringFragment('hello'); + expect(string.value).toBe('hello'); + expect(string.isComplete).toBe(true); + }); + }); + + describe('ArrayFragment', () => { + it('lifecycle', () => { + const array = new ArrayFragment(); + expect(array.items).toHaveLength(0); + expect(array.isComplete).toBe(false); + + array.addValue(new StringFragment('hello')); + expect(array.items).toEqual([new StringFragment('hello')]); + expect(array.isComplete).toBe(false); + }); + }); + + describe('RecordFragment', () => { + it('lifecycle', () => { + const record = new RecordFragment(); + expect(record.children).toEqual({}); + expect(record.isComplete).toBe(false); + + const attribute = new AttributeFragment( + 'name', + new StringFragment('chicken') + ); + record.addAttribute(attribute); + expect(record.children).toEqual({ name: new StringFragment('chicken') }); + expect(record.isComplete).toBe(false); + + const badAttribute = new AttributeFragment('type'); + expect(() => record.addAttribute(badAttribute)).toThrow( + new Error('The attribute fragment does not have a value.') + ); + expect(record.children).toEqual({ name: new StringFragment('chicken') }); + expect(record.isComplete).toBe(false); + }); + }); + + describe('AttributeFragment', () => { + it('lifecycle', () => { + const attribute = new AttributeFragment('name'); + expect(attribute.name).toBe('name'); + expect(attribute.value).toBeUndefined(); + expect(attribute.isComplete).toBe(false); + + const value = new StringFragment('chicken'); + attribute.addValue(value); + expect(attribute.value).toEqual(value); + expect(attribute.isComplete).toBe(true); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts new file mode 100644 index 00000000000000..03c79c9e99d0dc --- /dev/null +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -0,0 +1,166 @@ +import is from '@sindresorhus/is'; + +export type FragmentType = 'string' | 'array' | 'record' | 'attribute'; + +export interface FragmentCompatible { + readonly type: FragmentType; +} + +export class StringFragment implements FragmentCompatible { + readonly type: FragmentType = 'string'; + readonly isComplete = true; + constructor(readonly value: string) {} +} + +export class ArrayFragment implements FragmentCompatible { + readonly type: FragmentType = 'array'; + isComplete = false; + items: ValueFragment[] = []; + + constructor(items: ValueFragment[] = [], isComplete = false) { + this.items = items; + this.isComplete = isComplete; + } + + addValue(item: ValueFragment): void { + this.items.push(item); + } +} + +export class RecordFragment implements FragmentCompatible { + readonly type: FragmentType = 'record'; + isComplete = false; + children: ChildFragments; + + constructor(children: ChildFragments = {}, isComplete = false) { + this.children = children; + this.isComplete = isComplete; + } + + addAttribute(attrib: AttributeFragment): void { + if (!attrib.value) { + throw new Error('The attribute fragment does not have a value.'); + } + this.children[attrib.name] = attrib.value; + } +} + +export class AttributeFragment implements FragmentCompatible { + readonly type: FragmentType = 'attribute'; + readonly name: string; + value?: ValueFragment; + + constructor(name: string, value?: ValueFragment) { + this.name = name; + this.value = value; + } + + get isComplete(): boolean { + return is.truthy(this.value); + } + + addValue(item: ValueFragment): void { + this.value = item; + } +} + +export type ValueFragment = ArrayFragment | RecordFragment | StringFragment; +export type ChildFragments = Record; +export type Fragment = ValueFragment | AttributeFragment; + +export class Fragments { + private static typeError(expected: string, actual: string): Error { + return new Error(`Expected type ${expected}, but was ${actual}.`); + } + + private static checkType(expected: string, actual: string): void { + if (expected === actual) { + return; + } + throw Fragments.typeError(expected, actual); + } + + static safeAsValue(frag: FragmentCompatible): ValueFragment | undefined { + switch (frag.type) { + case 'string': + return Fragments.asString(frag); + break; + case 'array': + return Fragments.asArray(frag); + break; + case 'record': + return Fragments.asRecord(frag); + break; + default: + return undefined; + } + } + + static asValue(frag: FragmentCompatible): ValueFragment { + const value = Fragments.safeAsValue(frag); + if (value) { + return value; + } + throw new Error(`Unexpected fragment type. ${frag.type}`); + } + + static asFragment(frag: FragmentCompatible): Fragment { + const value = Fragments.safeAsValue(frag); + if (value) { + return value; + } + if (frag.type === 'attribute') { + return Fragments.asAttribute(frag); + } + throw new Error(`Unexpected fragment type. ${frag.type}`); + } + + static asString(frag: FragmentCompatible): StringFragment { + Fragments.checkType('string', frag.type); + Object.setPrototypeOf(frag, StringFragment.prototype); + return frag as StringFragment; + } + + static asArray(frag: FragmentCompatible): ArrayFragment { + Fragments.checkType('array', frag.type); + Object.setPrototypeOf(frag, ArrayFragment.prototype); + const array = frag as ArrayFragment; + for (let i = 0; i < array.items.length; i++) { + array.items[i] = Fragments.asValue(array.items[i]); + } + return array; + } + + static asRecord(frag: FragmentCompatible): RecordFragment { + Fragments.checkType('record', frag.type); + Object.setPrototypeOf(frag, RecordFragment.prototype); + const record = frag as RecordFragment; + for (const prop in record.children) { + const child = record.children[prop]; + record.children[prop] = Fragments.asValue(child); + } + return record; + } + + static safeAsAttribute( + frag: FragmentCompatible + ): AttributeFragment | undefined { + if (frag.type !== 'attribute') { + return undefined; + } + Object.setPrototypeOf(frag, AttributeFragment.prototype); + const attribute = frag as AttributeFragment; + if (attribute.value) { + attribute.value = Fragments.asValue(attribute.value); + } + return attribute; + } + + static asAttribute(frag: FragmentCompatible): AttributeFragment { + const attribute = Fragments.safeAsAttribute(frag); + if (attribute) { + return attribute; + } + throw this.typeError('attribute', frag.type); + } +} diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index f87a82d94ec28c..b1f9449847b36d 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -1,6 +1,6 @@ import { codeBlock } from 'common-tags'; +import { ArrayFragment, RecordFragment, StringFragment } from './fragments'; import { Ctx, parse } from './parser'; -import { ArrayFragment, RecordFragment, StringFragment } from './types'; describe('modules/manager/bazel-module/parser', () => { describe('Ctx', () => { diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 88d0e669821ca6..2f0a5b7460406a 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -1,6 +1,5 @@ import { lang, query as q } from 'good-enough-parser'; import { logger } from '../../../logger'; -import { supportedRulesRegex } from './rules'; import { ArrayFragment, AttributeFragment, @@ -8,10 +7,11 @@ import { Fragment, Fragments, RecordFragment, - Stack, StringFragment, ValueFragment, -} from './types'; +} from './fragments'; +import { supportedRulesRegex } from './rules'; +import { Stack } from './types'; // Represents the fields that the context must have. export interface CtxCompatible { diff --git a/lib/modules/manager/bazel-module/types.ts b/lib/modules/manager/bazel-module/types.ts index c957c5d137a18d..c0fd8f3e3d57f4 100644 --- a/lib/modules/manager/bazel-module/types.ts +++ b/lib/modules/manager/bazel-module/types.ts @@ -1,5 +1,3 @@ -import is from '@sindresorhus/is'; - export class Stack extends Array { static create(...items: Array): Stack { const stack = new Stack(); @@ -22,163 +20,3 @@ export class Stack extends Array { return c; } } - -export type FragmentType = 'string' | 'array' | 'record' | 'attribute'; - -export interface FragmentCompatible { - readonly type: FragmentType; -} - -export class StringFragment implements FragmentCompatible { - readonly type: FragmentType = 'string'; - readonly isComplete = true; - constructor(readonly value: string) {} -} - -export class ArrayFragment implements FragmentCompatible { - readonly type: FragmentType = 'array'; - isComplete = false; - items: ValueFragment[] = []; - - constructor(items: ValueFragment[] = [], isComplete = false) { - this.items = items; - this.isComplete = isComplete; - } - - addValue(item: ValueFragment): void { - this.items.push(item); - } -} - -export class RecordFragment implements FragmentCompatible { - readonly type: FragmentType = 'record'; - isComplete = false; - children: ChildFragments; - - constructor(children: ChildFragments = {}, isComplete = false) { - this.children = children; - this.isComplete = isComplete; - } - - addAttribute(attrib: AttributeFragment): void { - if (!attrib.value) { - throw new Error('The attribute fragment does not have a value.'); - } - this.children[attrib.name] = attrib.value; - } -} - -export class AttributeFragment implements FragmentCompatible { - readonly type: FragmentType = 'attribute'; - value?: ValueFragment; - constructor(readonly name: string) {} - - get isComplete(): boolean { - return is.truthy(this.value); - } - - addValue(item: ValueFragment): void { - this.value = item; - } -} - -export type ValueFragment = ArrayFragment | RecordFragment | StringFragment; -export type ChildFragments = Record; -export type Fragment = ValueFragment | AttributeFragment; - -export class Fragments { - private static typeError(expected: string, actual: string): Error { - return new Error(`Expected type ${expected}, but was ${actual}.`); - } - - private static checkType(expected: string, actual: string): void { - if (expected === actual) { - return; - } - throw Fragments.typeError(expected, actual); - } - - static safeAsValue(frag: FragmentCompatible): ValueFragment | undefined { - switch (frag.type) { - case 'string': - return Fragments.asString(frag); - break; - case 'array': - return Fragments.asArray(frag); - break; - case 'record': - return Fragments.asRecord(frag); - break; - default: - return undefined; - } - } - - static asValue(frag: FragmentCompatible): ValueFragment { - const value = Fragments.safeAsValue(frag); - if (value) { - return value; - } - throw new Error(`Unexpected fragment type. ${frag.type}`); - } - - static asFragment(frag: FragmentCompatible): Fragment { - const value = Fragments.safeAsValue(frag); - if (value) { - return value; - } - if (frag.type === 'attribute') { - return Fragments.asAttribute(frag); - } - throw new Error(`Unexpected fragment type. ${frag.type}`); - } - - static asString(frag: FragmentCompatible): StringFragment { - Fragments.checkType('string', frag.type); - Object.setPrototypeOf(frag, StringFragment.prototype); - return frag as StringFragment; - } - - static asArray(frag: FragmentCompatible): ArrayFragment { - Fragments.checkType('array', frag.type); - Object.setPrototypeOf(frag, ArrayFragment.prototype); - const array = frag as ArrayFragment; - for (let i = 0; i < array.items.length; i++) { - array.items[i] = Fragments.asValue(array.items[i]); - } - return array; - } - - static asRecord(frag: FragmentCompatible): RecordFragment { - Fragments.checkType('record', frag.type); - Object.setPrototypeOf(frag, RecordFragment.prototype); - const record = frag as RecordFragment; - for (const prop in record.children) { - const child = record.children[prop]; - record.children[prop] = Fragments.asValue(child); - } - return record; - } - - static safeAsAttribute( - frag: FragmentCompatible - ): AttributeFragment | undefined { - if (frag.type !== 'attribute') { - return undefined; - } - Object.setPrototypeOf(frag, AttributeFragment.prototype); - const attribute = frag as AttributeFragment; - if (attribute.value) { - attribute.value = Fragments.asValue(attribute.value); - } - return attribute; - } - - static asAttribute(frag: FragmentCompatible): AttributeFragment { - const attribute = Fragments.safeAsAttribute(frag); - if (attribute) { - return attribute; - } - throw this.typeError('attribute', frag.type); - } -} From a4d3e85960b507e900b4e051549cfdae157e357b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 18:12:33 -0600 Subject: [PATCH 10/80] Add tests for Fragments functions. --- .../manager/bazel-module/fragments.spec.ts | 50 +++++++++++++++++++ lib/modules/manager/bazel-module/fragments.ts | 6 +-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 2ff9c9a23ff721..0681588976ee7d 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -1,6 +1,7 @@ import { ArrayFragment, AttributeFragment, + Fragments, RecordFragment, StringFragment, } from './fragments'; @@ -62,4 +63,53 @@ describe('modules/manager/bazel-module/fragments', () => { expect(attribute.isComplete).toBe(true); }); }); + + describe('Fragments', () => { + it.each` + frag | exp + ${new StringFragment('hello')} | ${new StringFragment('hello')} + ${new ArrayFragment()} | ${new ArrayFragment()} + ${new RecordFragment()} | ${new RecordFragment()} + ${new AttributeFragment('name')} | ${undefined} + `('Fragments.safeAsValue($frag)', ({ frag, exp }) => { + const result = Fragments.safeAsValue(frag); + expect(result).toEqual(exp); + }); + + describe('asValue', () => { + it('returns the instance when it is a value fragment', () => { + const value = new StringFragment('hello'); + const result = Fragments.asValue(value); + expect(result).toEqual(value); + }); + + it('throws when it is not a value fragment', () => { + const attribute = new AttributeFragment('name'); + expect(() => Fragments.asValue(attribute)).toThrow( + new Error(`Unexpected fragment type: attribute`) + ); + }); + }); + + it.each` + frag | exp + ${new StringFragment('hello')} | ${new StringFragment('hello')} + ${new ArrayFragment()} | ${new ArrayFragment()} + ${new RecordFragment()} | ${new RecordFragment()} + ${new AttributeFragment('name')} | ${new AttributeFragment('name')} + `('Fragments.asFragment($frag)', ({ frag, exp }) => { + const result = Fragments.asFragment(frag); + expect(result).toEqual(exp); + }); + + it.each` + fn | frag + ${Fragments.asString} | ${new ArrayFragment()} + ${Fragments.asArray} | ${new RecordFragment()} + ${Fragments.asRecord} | ${new ArrayFragment()} + ${Fragments.asAttribute} | ${new ArrayFragment()} + `('$fn throws with $frag', ({ fn, frag }) => { + expect(() => fn(frag)).toThrow(); + }); + }); }); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 03c79c9e99d0dc..6d7dbfd2b28204 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -84,13 +84,10 @@ export class Fragments { switch (frag.type) { case 'string': return Fragments.asString(frag); - break; case 'array': return Fragments.asArray(frag); - break; case 'record': return Fragments.asRecord(frag); - break; default: return undefined; } @@ -101,7 +98,7 @@ export class Fragments { if (value) { return value; } - throw new Error(`Unexpected fragment type. ${frag.type}`); + throw new Error(`Unexpected fragment type: ${frag.type}`); } static asFragment(frag: FragmentCompatible): Fragment { @@ -112,6 +109,7 @@ export class Fragments { if (frag.type === 'attribute') { return Fragments.asAttribute(frag); } + // istanbul ignore next: can only get here if new type addded, but no impl throw new Error(`Unexpected fragment type. ${frag.type}`); } From f8c6cf96f6d46c2ec81a037ce560e88928040361 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 18:16:00 -0600 Subject: [PATCH 11/80] Test name change --- lib/modules/manager/bazel-module/fragments.spec.ts | 2 +- lib/modules/manager/bazel-module/fragments.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 0681588976ee7d..3aaec59c5bc5b7 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -8,7 +8,7 @@ import { describe('modules/manager/bazel-module/fragments', () => { describe('StringFragment', () => { - it('constructor ', () => { + it('lifecycle ', () => { const string = new StringFragment('hello'); expect(string.value).toBe('hello'); expect(string.isComplete).toBe(true); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 6d7dbfd2b28204..9ae4beafbdc7fc 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -110,7 +110,7 @@ export class Fragments { return Fragments.asAttribute(frag); } // istanbul ignore next: can only get here if new type addded, but no impl - throw new Error(`Unexpected fragment type. ${frag.type}`); + throw new Error(`Unexpected fragment type: ${frag.type}`); } static asString(frag: FragmentCompatible): StringFragment { From b1cd662b6495483369322f6068fdaa6d71f5c6b2 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 18:19:23 -0600 Subject: [PATCH 12/80] Rename types to stack. --- lib/modules/manager/bazel-module/parser.ts | 2 +- .../manager/bazel-module/{types.spec.ts => stack.spec.ts} | 4 ++-- lib/modules/manager/bazel-module/{types.ts => stack.ts} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename lib/modules/manager/bazel-module/{types.spec.ts => stack.spec.ts} (78%) rename lib/modules/manager/bazel-module/{types.ts => stack.ts} (100%) diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 2f0a5b7460406a..e07ab84aba1f53 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -11,7 +11,7 @@ import { ValueFragment, } from './fragments'; import { supportedRulesRegex } from './rules'; -import { Stack } from './types'; +import { Stack } from './stack'; // Represents the fields that the context must have. export interface CtxCompatible { diff --git a/lib/modules/manager/bazel-module/types.spec.ts b/lib/modules/manager/bazel-module/stack.spec.ts similarity index 78% rename from lib/modules/manager/bazel-module/types.spec.ts rename to lib/modules/manager/bazel-module/stack.spec.ts index 73171c06572982..d61245559c4c7b 100644 --- a/lib/modules/manager/bazel-module/types.spec.ts +++ b/lib/modules/manager/bazel-module/stack.spec.ts @@ -1,6 +1,6 @@ -import { Stack } from './types'; +import { Stack } from './stack'; -describe('modules/manager/bazel-module/types', () => { +describe('modules/manager/bazel-module/stack', () => { describe('Stack', () => { it.each` items | exp diff --git a/lib/modules/manager/bazel-module/types.ts b/lib/modules/manager/bazel-module/stack.ts similarity index 100% rename from lib/modules/manager/bazel-module/types.ts rename to lib/modules/manager/bazel-module/stack.ts From cd61684b7d2af3fae234e295d9549eaab735da31 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 18:22:24 -0600 Subject: [PATCH 13/80] Add missing test for Stack. --- lib/modules/manager/bazel-module/stack.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/stack.spec.ts b/lib/modules/manager/bazel-module/stack.spec.ts index d61245559c4c7b..81e55716e0e1ed 100644 --- a/lib/modules/manager/bazel-module/stack.spec.ts +++ b/lib/modules/manager/bazel-module/stack.spec.ts @@ -6,9 +6,16 @@ describe('modules/manager/bazel-module/stack', () => { items | exp ${[]} | ${undefined} ${['first', 'second']} | ${'second'} - `('get current for $items', ({ items, exp }) => { + `('safely get current for $items', ({ items, exp }) => { const stack = Stack.create(...items); expect(stack.safeCurrent).toBe(exp); }); + + it('current throws if no items', () => { + const stack = Stack.create(); + expect(() => stack.current).toThrow( + new Error('Requested current, but no value.') + ); + }); }); }); From 8cf6d5c57c4c429e2c80fbeeb599879658b26280 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 26 Apr 2023 19:04:54 -0600 Subject: [PATCH 14/80] Add missing tests for context --- .../manager/bazel-module/context.spec.ts | 112 ++++++++++++ lib/modules/manager/bazel-module/context.ts | 137 +++++++++++++++ .../manager/bazel-module/parser.spec.ts | 61 +------ lib/modules/manager/bazel-module/parser.ts | 165 +----------------- 4 files changed, 253 insertions(+), 222 deletions(-) create mode 100644 lib/modules/manager/bazel-module/context.spec.ts create mode 100644 lib/modules/manager/bazel-module/context.ts diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts new file mode 100644 index 00000000000000..3a46d293e40ac5 --- /dev/null +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -0,0 +1,112 @@ +import { Ctx } from './context'; +import { ArrayFragment, RecordFragment, StringFragment } from './fragments'; + +describe('modules/manager/bazel-module/context', () => { + describe('Ctx', () => { + it('construct simple bazel_dep', () => { + const ctx = new Ctx() + .startRule('bazel_dep') + .startAttribute('name') + .addString('rules_foo') + .startAttribute('version') + .addString('1.2.3') + .endRule(); + + expect(ctx.results).toEqual([ + new RecordFragment( + { + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + }, + true + ), + ]); + }); + + it('construct a rule with array arg', () => { + const ctx = new Ctx() + .startRule('foo_library') + .startAttribute('name') + .addString('my_library') + .startAttribute('srcs') + .startArray() + .addArrayItem('first') + .addArrayItem('second') + .endArray() + .startAttribute('tags') + .startRule('get_tags') + .endRule() + .endRule(); + + expect(ctx.results).toEqual([ + new RecordFragment( + { + rule: new StringFragment('foo_library'), + name: new StringFragment('my_library'), + srcs: new ArrayFragment( + [new StringFragment('first'), new StringFragment('second')], + true + ), + tags: new RecordFragment( + { rule: new StringFragment('get_tags') }, + true + ), + }, + true + ), + ]); + }); + + describe('currentRecord', () => { + it('returns the record fragment if it is current', () => { + const ctx = new Ctx().startRecord(); + expect(ctx.currentRecord).toEqual(new RecordFragment()); + }); + + it('throws if it the current is not a record fragment', () => { + const ctx = new Ctx().startArray(); + expect(() => ctx.currentRecord).toThrow( + new Error('Requested current record, but does not exist.') + ); + }); + }); + + describe('currentArray', () => { + it('returns the array fragment if it is current', () => { + const ctx = new Ctx().startArray(); + expect(ctx.currentArray).toEqual(new ArrayFragment()); + }); + + it('throws if it the current is not a record fragment', () => { + const ctx = new Ctx().startRecord(); + expect(() => ctx.currentArray).toThrow( + new Error('Requested current array, but does not exist.') + ); + }); + }); + + it('throws if add an attribute without a record', () => { + const ctx = new Ctx().startAttribute('name'); + expect(() => ctx.addString('chicken')).toThrow( + new Error('Processing an attribute but there is no parent.') + ); + }); + }); + + describe('Ctx.from', () => { + it('adds the appropriate prototype to the context and referenced fragments', () => { + // Ensure that we have values in results (bazel_dep) and the stack (foo_library). + const ctx = new Ctx() + .startRule('bazel_dep') + .startAttribute('name') + .addString('rules_foo') + .startAttribute('version') + .addString('1.2.3') + .endRule() + .startRule('foo_library'); + const result = Ctx.from(ctx); + expect(result).toEqual(ctx); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts new file mode 100644 index 00000000000000..d9dcfa7cb77d9b --- /dev/null +++ b/lib/modules/manager/bazel-module/context.ts @@ -0,0 +1,137 @@ +import { + ArrayFragment, + AttributeFragment, + ChildFragments, + Fragment, + Fragments, + RecordFragment, + StringFragment, + ValueFragment, +} from './fragments'; +import { Stack } from './stack'; + +// Represents the fields that the context must have. +export interface CtxCompatible { + results: ValueFragment[]; + stack: Stack; +} + +export class Ctx implements CtxCompatible { + results: ValueFragment[] = []; + stack = Stack.create(); + + // This exists because the good-enough-parser gives a cloned instance of our + // Ctx. It is missing the Ctx prototype. This function adds the proper prototype + // to the context and the items referenced by the context. + static from(obj: CtxCompatible): Ctx { + Object.setPrototypeOf(obj, Ctx.prototype); + const ctx = obj as Ctx; + const stackItems = ctx.stack.map((item) => Fragments.asFragment(item)); + ctx.stack = Stack.create(...stackItems); + ctx.results = ctx.results.map((item) => Fragments.asRecord(item)); + return ctx; + } + + get currentRecord(): RecordFragment { + const current = this.stack.current; + if (current instanceof RecordFragment) { + return current; + } + throw new Error('Requested current record, but does not exist.'); + } + + get currentArray(): ArrayFragment { + const current = this.stack.current; + if (current instanceof ArrayFragment) { + return current; + } + throw new Error('Requested current array, but does not exist.'); + } + + private popStack(): void { + const current = this.stack.pop(); + // istanbul ignore if: we should never get here + if (!current) { + return; + } + const value = Fragments.safeAsValue(current); + const attribute = Fragments.safeAsAttribute(current); + const newCurrent = this.stack.safeCurrent; + if (value) { + if (newCurrent && 'addValue' in newCurrent) { + newCurrent.addValue(value); + return; + } + if (!newCurrent) { + this.results.push(value); + return; + } + } else if (attribute) { + if (newCurrent && 'addAttribute' in newCurrent) { + newCurrent.addAttribute(attribute); + return; + } + if (!newCurrent) { + throw new Error('Processing an attribute but there is no parent.'); + } + } + // istanbul ignore next: catching future mistakes + throw new Error('We are in a bad place.'); + } + + private processStack(): Ctx { + let current = this.stack.safeCurrent; + while (current?.isComplete) { + this.popStack(); + current = this.stack.safeCurrent; + } + return this; + } + + addString(value: string): Ctx { + this.stack.push(new StringFragment(value)); + return this.processStack(); + } + + startRecord(children: ChildFragments = {}): Ctx { + const record = new RecordFragment(children); + this.stack.push(record); + return this; + } + + endRecord(): Ctx { + const record = this.currentRecord; + record.isComplete = true; + return this.processStack(); + } + + startRule(name: string): Ctx { + return this.startRecord({ rule: new StringFragment(name) }); + } + + endRule(): Ctx { + return this.endRecord(); + } + + startAttribute(name: string): Ctx { + this.stack.push(new AttributeFragment(name)); + return this.processStack(); + } + + startArray(): Ctx { + this.stack.push(new ArrayFragment()); + return this.processStack(); + } + + addArrayItem(value: string): Ctx { + const array = this.currentArray; + array.items.push(new StringFragment(value)); + return this; + } + + endArray(): Ctx { + const array = this.currentArray; + array.isComplete = true; + return this.processStack(); + } +} diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index b1f9449847b36d..71f55465e35917 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -1,65 +1,8 @@ import { codeBlock } from 'common-tags'; -import { ArrayFragment, RecordFragment, StringFragment } from './fragments'; -import { Ctx, parse } from './parser'; +import { RecordFragment, StringFragment } from './fragments'; +import { parse } from './parser'; describe('modules/manager/bazel-module/parser', () => { - describe('Ctx', () => { - it('construct bazel_dep', () => { - const ctx = new Ctx() - .startRule('bazel_dep') - .startAttribute('name') - .addString('rules_foo') - .startAttribute('version') - .addString('1.2.3') - .endRule(); - - expect(ctx.results).toEqual([ - new RecordFragment( - { - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), - }, - true - ), - ]); - }); - - it('construct a rule with array arg', () => { - const ctx = new Ctx() - .startRule('foo_library') - .startAttribute('name') - .addString('my_library') - .startAttribute('srcs') - .startArray() - .addArrayItem('first') - .addArrayItem('second') - .endArray() - .startAttribute('tags') - .startRule('get_tags') - .endRule() - .endRule(); - - expect(ctx.results).toEqual([ - new RecordFragment( - { - rule: new StringFragment('foo_library'), - name: new StringFragment('my_library'), - srcs: new ArrayFragment( - [new StringFragment('first'), new StringFragment('second')], - true - ), - tags: new RecordFragment( - { rule: new StringFragment('get_tags') }, - true - ), - }, - true - ), - ]); - }); - }); - describe('parse', () => { it('finds simple bazel_dep', () => { const input = codeBlock` diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index e07ab84aba1f53..8a40add9f99e48 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -1,164 +1,8 @@ import { lang, query as q } from 'good-enough-parser'; import { logger } from '../../../logger'; -import { - ArrayFragment, - AttributeFragment, - ChildFragments, - Fragment, - Fragments, - RecordFragment, - StringFragment, - ValueFragment, -} from './fragments'; +import { Ctx } from './context'; +import type { ValueFragment } from './fragments'; import { supportedRulesRegex } from './rules'; -import { Stack } from './stack'; - -// Represents the fields that the context must have. -export interface CtxCompatible { - results: ValueFragment[]; - stack: Stack; -} - -export class Ctx implements CtxCompatible { - results: ValueFragment[] = []; - stack = Stack.create(); - - // This exists because the good-enough-parser gives a cloned instance of our - // Ctx instance. This instance is missing the Ctx prototype. - static from(obj: CtxCompatible): Ctx { - Object.setPrototypeOf(obj, Ctx.prototype); - const ctx = obj as Ctx; - const stackItems = ctx.stack.map((item) => Fragments.asFragment(item)); - ctx.stack = Stack.create(...stackItems); - ctx.results = ctx.results.map((item) => Fragments.asRecord(item)); - return ctx; - } - - private newError(msg: string): Error { - return new Error(`${msg} ctx: ${JSON.stringify(this)}`); - } - - get currentRecord(): RecordFragment { - const current = this.stack.current; - if (current instanceof RecordFragment) { - return current; - } - throw this.newError('Requested current record, but does not exist.'); - } - - get currentAttribute(): AttributeFragment { - const current = this.stack.current; - if (current instanceof AttributeFragment) { - return current; - } - throw this.newError('Requested current attribute, but does not exist.'); - } - - get currentArray(): ArrayFragment { - const current = this.stack.current; - if (current instanceof ArrayFragment) { - return current; - } - throw this.newError('Requested current array, but does not exist.'); - } - - private popStack(): void { - const current = this.stack.pop(); - if (!current) { - return; - } - const value = Fragments.safeAsValue(current); - const attribute = Fragments.safeAsAttribute(current); - const newCurrent = this.stack.safeCurrent; - if (value) { - if (newCurrent && 'addValue' in newCurrent) { - newCurrent.addValue(value); - return; - } - if (!newCurrent) { - this.results.push(value); - return; - } - } else if (attribute) { - if (newCurrent && 'addAttribute' in newCurrent) { - newCurrent.addAttribute(attribute); - return; - } - if (!newCurrent) { - throw this.newError('Processing an attribute but there is no parent.'); - } - } else { - throw this.newError('We are in a bad place.'); - } - } - - private processStack(): Ctx { - let current = this.stack.safeCurrent; - while (current?.isComplete) { - this.popStack(); - current = this.stack.safeCurrent; - } - return this; - } - - addString(value: string): Ctx { - this.stack.push(new StringFragment(value)); - return this.processStack(); - } - - startRecord(children: ChildFragments): Ctx { - const record = new RecordFragment(children); - this.stack.push(record); - return this; - } - - endRecord(): Ctx { - const record = this.currentRecord; - record.isComplete = true; - return this.processStack(); - } - - startRule(name: string): Ctx { - return this.startRecord({ rule: new StringFragment(name) }); - } - - endRule(): Ctx { - return this.endRecord(); - } - - startAttribute(name: string): Ctx { - this.stack.push(new AttributeFragment(name)); - return this.processStack(); - } - - // endAttribute(): Ctx { - // const attrib = this.currentAttribute; - // if (!attrib.value) { - // throw this.newError(`No value was set for the attribute. ${attrib.name}`); - // } - // this.stack.pop(); - // const record = this.currentRecord; - // record.children[attrib.name] = attrib.value; - // return this; - // } - - startArray(): Ctx { - this.stack.push(new ArrayFragment()); - return this.processStack(); - } - - addArrayItem(value: string): Ctx { - const array = this.currentArray; - array.items.push(new StringFragment(value)); - return this; - } - - endArray(): Ctx { - const array = this.currentArray; - array.isComplete = true; - return this.processStack(); - } -} /** * Matches key-value pairs: @@ -227,8 +71,3 @@ export function parse( return result; } - -// // export class Parser { -// // static readonly starlark = lang.createLang('starlark'); -// // constructor(readonly input: string, readonly packageFile?: string) {} -// // } From a0dd10ee4afd39b2e2a98ec1bc573c1382eb1137 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 07:05:38 -0600 Subject: [PATCH 15/80] Add isRule to RecordFragment --- lib/modules/manager/bazel-module/fragments.spec.ts | 12 ++++++++++++ lib/modules/manager/bazel-module/fragments.ts | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 3aaec59c5bc5b7..8a936514bbef8a 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -48,6 +48,18 @@ describe('modules/manager/bazel-module/fragments', () => { expect(record.children).toEqual({ name: new StringFragment('chicken') }); expect(record.isComplete).toBe(false); }); + + it.each` + record | type | exp + ${new RecordFragment()} | ${undefined} | ${false} + ${new RecordFragment()} | ${'bazel_dep'} | ${false} + ${new RecordFragment({ rule: new StringFragment('bazel_dep') })} | ${undefined} | ${true} + ${new RecordFragment({ rule: new StringFragment('bazel_dep') })} | ${'bazel_dep'} | ${true} + ${new RecordFragment({ rule: new StringFragment('foo') })} | ${'bazel_dep'} | ${false} + ${new RecordFragment({ rule: new ArrayFragment() })} | ${'bazel_dep'} | ${false} + `('.isRule', ({ record, type, exp }) => { + expect(record.isRule(type)).toBe(exp); + }); }); describe('AttributeFragment', () => { diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 9ae4beafbdc7fc..d74defe7242cf0 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -43,6 +43,17 @@ export class RecordFragment implements FragmentCompatible { } this.children[attrib.name] = attrib.value; } + + isRule(type?: string): boolean { + let result = 'rule' in this.children; + if (type) { + const ruleValue = this.children['rule']; + const ruleValueStr = + ruleValue instanceof StringFragment ? ruleValue : undefined; + result = result && ruleValueStr?.value === type; + } + return result; + } } export class AttributeFragment implements FragmentCompatible { From 818203d1074b67feeddfed36a67177ced79b8f1b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 08:08:07 -0600 Subject: [PATCH 16/80] Add instanceExists --- lib/modules/manager/bazel-module/filters.spec.ts | 13 +++++++++++++ lib/modules/manager/bazel-module/filters.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 lib/modules/manager/bazel-module/filters.spec.ts create mode 100644 lib/modules/manager/bazel-module/filters.ts diff --git a/lib/modules/manager/bazel-module/filters.spec.ts b/lib/modules/manager/bazel-module/filters.spec.ts new file mode 100644 index 00000000000000..baf7bf3803d71e --- /dev/null +++ b/lib/modules/manager/bazel-module/filters.spec.ts @@ -0,0 +1,13 @@ +import { instanceExists } from './filters'; + +describe('modules/manager/bazel-module/filters', () => { + it('instanceExists', () => { + const array: Array = [ + 'first', + null, + 'second', + undefined, + ]; + expect(array.filter(instanceExists)).toEqual(['first', 'second']); + }); +}); diff --git a/lib/modules/manager/bazel-module/filters.ts b/lib/modules/manager/bazel-module/filters.ts new file mode 100644 index 00000000000000..30d3ff5746a7f6 --- /dev/null +++ b/lib/modules/manager/bazel-module/filters.ts @@ -0,0 +1,13 @@ +import is from '@sindresorhus/is'; + +// Filter out missing values in a typesafe manner. +// Inspired by https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array +export function instanceExists( + value: Value | null | undefined +): value is Value { + if (is.falsy(value)) { + return false; + } + const placeholder: Value = value; + return is.truthy(placeholder); +} From 711b1cd77a4626ced6608829295bbb9c81c9ffd1 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 12:55:53 -0600 Subject: [PATCH 17/80] Add StarlarkBoolean --- .../manager/bazel-module/starlark.spec.ts | 13 ++++++++++++ lib/modules/manager/bazel-module/starlark.ts | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 lib/modules/manager/bazel-module/starlark.spec.ts create mode 100644 lib/modules/manager/bazel-module/starlark.ts diff --git a/lib/modules/manager/bazel-module/starlark.spec.ts b/lib/modules/manager/bazel-module/starlark.spec.ts new file mode 100644 index 00000000000000..f015d179f5c2bd --- /dev/null +++ b/lib/modules/manager/bazel-module/starlark.spec.ts @@ -0,0 +1,13 @@ +import { StarlarkBoolean } from './starlark'; + +describe('modules/manager/bazel-module/starlark', () => { + describe('StarlarkBoolean', () => { + it('asBoolean', () => { + expect(StarlarkBoolean.asBoolean('True')).toBe(true); + expect(StarlarkBoolean.asBoolean('False')).toBe(false); + expect(() => StarlarkBoolean.asBoolean('bad')).toThrow( + new Error('Invalid Starlark boolean string: bad') + ); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/starlark.ts b/lib/modules/manager/bazel-module/starlark.ts new file mode 100644 index 00000000000000..35f922bae28bd3 --- /dev/null +++ b/lib/modules/manager/bazel-module/starlark.ts @@ -0,0 +1,21 @@ +export class StarlarkBoolean { + static readonly stringMapping: ReadonlyMap = new Map< + string, + boolean + >([ + ['True', true], + ['False', false], + ]); + + static readonly stringValues = Array.from( + StarlarkBoolean.stringMapping.keys() + ); + + static asBoolean(value: string): boolean { + const result = StarlarkBoolean.stringMapping.get(value); + if (result !== undefined) { + return result; + } + throw new Error(`Invalid Starlark boolean string: ${value}`); + } +} From 326f2937567a6250c5bbeea2586cad951890d3ea Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 13:48:51 -0600 Subject: [PATCH 18/80] Save work. Need to fix extractPackageFile tests --- lib/modules/manager/bazel-module/context.ts | 6 +++ .../manager/bazel-module/extract.spec.ts | 26 ++++++++++ lib/modules/manager/bazel-module/extract.ts | 49 ++++++------------- .../manager/bazel-module/fragments.spec.ts | 22 +++++++++ lib/modules/manager/bazel-module/fragments.ts | 32 +++++++++++- .../manager/bazel-module/parser.spec.ts | 12 ++++- lib/modules/manager/bazel-module/parser.ts | 17 +++++-- .../bazel-module/rules/bazel-dep.spec.ts | 44 +++++++++++++++++ .../manager/bazel-module/rules/bazel-dep.ts | 42 +++++++++++----- .../manager/bazel-module/rules/index.ts | 40 +-------------- 10 files changed, 200 insertions(+), 90 deletions(-) create mode 100644 lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index d9dcfa7cb77d9b..ee077f3f70a4ad 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -1,6 +1,7 @@ import { ArrayFragment, AttributeFragment, + BooleanFragment, ChildFragments, Fragment, Fragments, @@ -93,6 +94,11 @@ export class Ctx implements CtxCompatible { return this.processStack(); } + addBoolean(value: string | boolean): Ctx { + this.stack.push(new BooleanFragment(value)); + return this.processStack(); + } + startRecord(children: ChildFragments = {}): Ctx { const record = new RecordFragment(children); this.stack.push(record); diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 93465dd28d963a..cc67e68588c8f7 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -1,3 +1,5 @@ +import { codeBlock } from 'common-tags'; +import { BazelDatasource } from '../../datasource/bazel'; import { extractPackageFile } from '.'; describe('modules/manager/bazel-module/extract', () => { @@ -6,5 +8,29 @@ describe('modules/manager/bazel-module/extract', () => { const res = extractPackageFile('blahhhhh:foo:@what\n', 'MODULE.bazel'); expect(res).toBeNull(); }); + + it('returns dependencies', () => { + const input = codeBlock` + bazel_dep(name = "rules_foo", version = "1.2.3") + bazel_dep(name = "rules_bar", version = "1.0.0", dev_dependency = True) + `; + const result = extractPackageFile(input, 'MODULE.bazel'); + expect(result).toEqual({ + deps: [ + { + datasource: BazelDatasource.id, + depType: 'bazel_dep', + depName: 'rules_foo', + currentValue: '1.2.3', + }, + { + datasource: BazelDatasource.id, + depType: 'bazel_dep', + depName: 'rules_bar', + currentValue: '1.0.0', + }, + ], + }); + }); }); }); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 6b67c5f37b8a6a..812515e25b4dee 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,44 +1,25 @@ -import { parse } from '../bazel/parser'; -import type { RecordFragment } from '../bazel/types'; -import type { PackageDependency, PackageFileContent } from '../types'; - -// import { parse } from './parser'; -// import { extractDepsFromFragment } from './rules'; -// import type { RecordFragment } from './types'; +import type { PackageFileContent } from '../types'; +import { instanceExists } from './filters'; +import { RecordFragment } from './fragments'; +import { parse } from './parser'; +import { BazelDepRecordToPackageDependency } from './rules'; export function extractPackageFile( content: string, packageFile: string ): PackageFileContent | null { - const deps: PackageDependency[] = []; - - const fragments: RecordFragment[] | null = parse(content, packageFile); + const fragments = parse(content, packageFile); if (!fragments) { return null; } - - // for (let idx = 0; idx < fragments.length; idx += 1) { - // const fragment = fragments[idx]; - // for (const dep of extractDepsFromFragment(fragment)) { - // dep.managerData = { idx }; - - // // // Selectively provide `replaceString` in order - // // // to auto-replace functionality work correctly. - // // const replaceString = fragment.value; - // // if ( - // // replaceString.startsWith('container_pull') || - // // replaceString.startsWith('oci_pull') || - // // replaceString.startsWith('git_repository') || - // // replaceString.startsWith('go_repository') - // // ) { - // // if (dep.currentValue && dep.currentDigest) { - // // dep.replaceString = replaceString; - // // } - // // } - - // deps.push(dep); - // } - // } - + const deps = fragments + .filter((value) => value instanceof RecordFragment) + .map((value) => value as RecordFragment) + .filter((record) => record.isRule('bazel_dep')) + .map((record) => { + const result = BazelDepRecordToPackageDependency.safeParse(record); + return result.success ? result.data : undefined; + }) + .filter(instanceExists); return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 8a936514bbef8a..80d465dc082cd9 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -1,6 +1,7 @@ import { ArrayFragment, AttributeFragment, + BooleanFragment, Fragments, RecordFragment, StringFragment, @@ -15,6 +16,27 @@ describe('modules/manager/bazel-module/fragments', () => { }); }); + describe('BooleanFragment', () => { + it('lifecycle ', () => { + const bool = new BooleanFragment(true); + expect(bool.value).toBe(true); + expect(bool.isComplete).toBe(true); + }); + + it.each` + a | exp + ${true} | ${true} + ${false} | ${false} + ${'True'} | ${true} + ${'False'} | ${false} + `('new BooleanFragment($a)', ({ a, exp }) => { + const bool = new BooleanFragment(a); + expect(bool.value).toBe(exp); + }); + + it.todo('Add test for invalid string'); + }); + describe('ArrayFragment', () => { it('lifecycle', () => { const array = new ArrayFragment(); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index d74defe7242cf0..4b82d584d33267 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -1,6 +1,12 @@ import is from '@sindresorhus/is'; +import { StarlarkBoolean } from './starlark'; -export type FragmentType = 'string' | 'array' | 'record' | 'attribute'; +export type FragmentType = + | 'string' + | 'boolean' + | 'array' + | 'record' + | 'attribute'; export interface FragmentCompatible { readonly type: FragmentType; @@ -12,6 +18,16 @@ export class StringFragment implements FragmentCompatible { constructor(readonly value: string) {} } +export class BooleanFragment implements FragmentCompatible { + readonly type: FragmentType = 'boolean'; + readonly isComplete = true; + readonly value: boolean; + constructor(input: boolean | string) { + this.value = + typeof input === 'string' ? StarlarkBoolean.asBoolean(input) : input; + } +} + export class ArrayFragment implements FragmentCompatible { readonly type: FragmentType = 'array'; isComplete = false; @@ -75,7 +91,11 @@ export class AttributeFragment implements FragmentCompatible { } } -export type ValueFragment = ArrayFragment | RecordFragment | StringFragment; +export type ValueFragment = + | ArrayFragment + | BooleanFragment + | RecordFragment + | StringFragment; export type ChildFragments = Record; export type Fragment = ValueFragment | AttributeFragment; @@ -95,6 +115,8 @@ export class Fragments { switch (frag.type) { case 'string': return Fragments.asString(frag); + case 'boolean': + return Fragments.asBoolean(frag); case 'array': return Fragments.asArray(frag); case 'record': @@ -124,6 +146,12 @@ export class Fragments { throw new Error(`Unexpected fragment type: ${frag.type}`); } + static asBoolean(frag: FragmentCompatible): BooleanFragment { + Fragments.checkType('boolean', frag.type); + Object.setPrototypeOf(frag, BooleanFragment.prototype); + return frag as BooleanFragment; + } + static asString(frag: FragmentCompatible): StringFragment { Fragments.checkType('string', frag.type); Object.setPrototypeOf(frag, StringFragment.prototype); diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index 71f55465e35917..8530fbc248913e 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -1,5 +1,5 @@ import { codeBlock } from 'common-tags'; -import { RecordFragment, StringFragment } from './fragments'; +import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; import { parse } from './parser'; describe('modules/manager/bazel-module/parser', () => { @@ -7,6 +7,7 @@ describe('modules/manager/bazel-module/parser', () => { it('finds simple bazel_dep', () => { const input = codeBlock` bazel_dep(name = "rules_foo", version = "1.2.3") + bazel_dep(name = "rules_bar", version = "1.0.0", dev_dependency = True) `; const res = parse(input, 'MODULE.bazel'); expect(res).toEqual([ @@ -18,6 +19,15 @@ describe('modules/manager/bazel-module/parser', () => { }, true ), + new RecordFragment( + { + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_bar'), + version: new StringFragment('1.0.0'), + dev_dependency: new BooleanFragment(true), + }, + true + ), ]); }); }); diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 8a40add9f99e48..392318cec6fb15 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -1,8 +1,12 @@ import { lang, query as q } from 'good-enough-parser'; import { logger } from '../../../logger'; +import { regEx } from '../../../util/regex'; import { Ctx } from './context'; import type { ValueFragment } from './fragments'; import { supportedRulesRegex } from './rules'; +import { StarlarkBoolean } from './starlark'; + +const booleanValuesRegex = regEx(`^${StarlarkBoolean.stringValues.join('|')}$`); /** * Matches key-value pairs: @@ -24,9 +28,14 @@ const kwParams = q return Ctx.from(ctx).startAttribute(token.value); }) .op('=') - .str((ctx, token) => { - return Ctx.from(ctx).addString(token.value); - }); + .alt( + q.str((ctx, token) => { + return Ctx.from(ctx).addString(token.value); + }), + q.sym(booleanValuesRegex, (ctx, token) => { + return Ctx.from(ctx).addBoolean(token.value); + }) + ); const moduleRules = q .sym(supportedRulesRegex, (ctx, token) => { @@ -66,7 +75,7 @@ export function parse( result = parsedResult.results; } } catch (err) /* istanbul ignore next */ { - logger.debug({ err, packageFile }, 'Bazel parsing error'); + logger.debug({ err, packageFile }, 'Bazel module parsing error'); } return result; diff --git a/lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts new file mode 100644 index 00000000000000..108009fb13e0bb --- /dev/null +++ b/lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts @@ -0,0 +1,44 @@ +import { BazelDatasource } from '../../../datasource/bazel'; +import { BooleanFragment, RecordFragment, StringFragment } from '../fragments'; +import { BazelDepRecord, BazelDepRecordToPackageDependency } from './bazel-dep'; + +describe('modules/manager/bazel-module/rules/bazel-dep', () => { + describe('BazelDepFragment', () => { + it('parses record fragment without dev_dependency', () => { + const record = new RecordFragment({ + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + }); + expect(() => BazelDepRecord.parse(record)).not.toThrow(); + }); + + it('parses record fragment with dev_dependency', () => { + const record = new RecordFragment({ + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + dev_dependency: new BooleanFragment(true), + }); + expect(() => BazelDepRecord.parse(record)).not.toThrow(); + }); + }); + + describe('BazelDepRecordToPackageDependency', () => { + it('transforms a record fragment', () => { + const record = new RecordFragment({ + rule: new StringFragment('bazel_dep'), + name: new StringFragment('rules_foo'), + version: new StringFragment('1.2.3'), + dev_dependency: new BooleanFragment(true), + }); + const result = BazelDepRecordToPackageDependency.parse(record); + expect(result).toEqual({ + datasource: BazelDatasource.id, + depType: 'bazel_dep', + depName: 'rules_foo', + currentValue: '1.2.3', + }); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/rules/bazel-dep.ts b/lib/modules/manager/bazel-module/rules/bazel-dep.ts index c529917012529e..332475482c675d 100644 --- a/lib/modules/manager/bazel-module/rules/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/rules/bazel-dep.ts @@ -2,21 +2,41 @@ import { z } from 'zod'; import { BazelDatasource } from '../../../datasource/bazel'; import type { PackageDependency } from '../../types'; -export const BazelDepTarget = z.object({ - rule: z.enum(['bazel_dep']), - name: z.string(), - version: z.string(), - dev_dependency: z.boolean().optional(), +const StringFragmentSchema = z.object({ + type: z.enum(['string']), + isComplete: z.boolean(), + value: z.string(), }); -export const BazelDepToPackageDependency = BazelDepTarget.transform( - ({ rule, name, version, dev_dependency }): PackageDependency[] => { +const BooleanFragmentSchema = z.object({ + type: z.enum(['boolean']), + isComplete: z.boolean(), + value: z.boolean(), +}); + +export const BazelDepRecord = z.object({ + type: z.enum(['record']), + isComplete: z.boolean(), + children: z.object({ + rule: z.object({ + type: z.enum(['string']), + isComplete: z.boolean(), + value: z.enum(['bazel_dep']), + }), + name: StringFragmentSchema, + version: StringFragmentSchema, + dev_dependency: BooleanFragmentSchema.optional(), + }), +}); + +export const BazelDepRecordToPackageDependency = BazelDepRecord.transform( + ({ children: { rule, name, version } }): PackageDependency => { const dep: PackageDependency = { datasource: BazelDatasource.id, - depType: rule, - depName: name, - currentValue: version, + depType: rule.value, + depName: name.value, + currentValue: version.value, }; - return [dep]; + return dep; } ); diff --git a/lib/modules/manager/bazel-module/rules/index.ts b/lib/modules/manager/bazel-module/rules/index.ts index bfa8c3a1401e10..a00fb9f3b1b8a5 100644 --- a/lib/modules/manager/bazel-module/rules/index.ts +++ b/lib/modules/manager/bazel-module/rules/index.ts @@ -1,43 +1,7 @@ import { regEx } from '../../../../util/regex'; +import { BazelDepRecord, BazelDepRecordToPackageDependency } from './bazel-dep'; const supportedRules = ['bazel_dep']; export const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); -// import type { Fragment, FragmentData } from '../../bazel/types'; -// import type { PackageDependency } from '../../types'; -// import { BazelDepTarget } from './bazel-dep'; - -// export function extractDepsFromFragment( -// fragment: Fragment -// ): PackageDependency[] { -// const fragmentData = extract(fragment); -// return extractDepsFromFragmentData(fragmentData); -// } - -// function extract(fragment: Fragment): FragmentData { -// if (fragment.type === 'string') { -// return fragment.value; -// } - -// if (fragment.type === 'record') { -// const { children } = fragment; -// const result: Record = {}; -// for (const [key, value] of Object.entries(children)) { -// result[key] = extract(value); -// } -// return result; -// } - -// return fragment.children.map(extract); -// } - -// function extractDepsFromFragmentData( -// fragmentData: FragmentData -// ): PackageDependency[] { -// const res = BazelDepTarget.safeParse(fragmentData); -// if (!res.success) { -// return []; -// } -// return res.data; -// } -// +export { BazelDepRecord, BazelDepRecordToPackageDependency }; From 25c3cd2be73c148d1ff405f7863db7ba4141d928 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 17:00:16 -0600 Subject: [PATCH 19/80] Save work. --- .../{rules => }/bazel-dep.spec.ts | 9 ++-- lib/modules/manager/bazel-module/bazel-dep.ts | 46 +++++++++++++++++++ lib/modules/manager/bazel-module/extract.ts | 32 ++++++++++++- lib/modules/manager/bazel-module/fragments.ts | 15 ++++++ lib/modules/manager/bazel-module/parser.ts | 3 +- .../manager/bazel-module/rules/bazel-dep.ts | 42 ----------------- .../manager/bazel-module/rules/index.spec.ts | 16 ------- .../manager/bazel-module/rules/index.ts | 7 --- 8 files changed, 99 insertions(+), 71 deletions(-) rename lib/modules/manager/bazel-module/{rules => }/bazel-dep.spec.ts (85%) create mode 100644 lib/modules/manager/bazel-module/bazel-dep.ts delete mode 100644 lib/modules/manager/bazel-module/rules/bazel-dep.ts delete mode 100644 lib/modules/manager/bazel-module/rules/index.spec.ts delete mode 100644 lib/modules/manager/bazel-module/rules/index.ts diff --git a/lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts similarity index 85% rename from lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts rename to lib/modules/manager/bazel-module/bazel-dep.spec.ts index 108009fb13e0bb..76e60a8ab76b84 100644 --- a/lib/modules/manager/bazel-module/rules/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -1,8 +1,8 @@ -import { BazelDatasource } from '../../../datasource/bazel'; -import { BooleanFragment, RecordFragment, StringFragment } from '../fragments'; +import { BazelDatasource } from '../../datasource/bazel'; import { BazelDepRecord, BazelDepRecordToPackageDependency } from './bazel-dep'; +import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; -describe('modules/manager/bazel-module/rules/bazel-dep', () => { +describe('modules/manager/bazel-module/bazel-dep', () => { describe('BazelDepFragment', () => { it('parses record fragment without dev_dependency', () => { const record = new RecordFragment({ @@ -20,7 +20,8 @@ describe('modules/manager/bazel-module/rules/bazel-dep', () => { version: new StringFragment('1.2.3'), dev_dependency: new BooleanFragment(true), }); - expect(() => BazelDepRecord.parse(record)).not.toThrow(); + const result = BazelDepRecord.parse(record); + expect(result).toBeInstanceOf(RecordFragment); }); }); diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts new file mode 100644 index 00000000000000..d1573f86cb1770 --- /dev/null +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import { BazelDatasource } from '../../datasource/bazel'; +import type { PackageDependency } from '../types'; +import { Fragments, RecordFragment } from './fragments'; + +// TODO: Move schema to static properties on the XXXFragment classes. + +const StringFragmentSchema = z.object({ + type: z.enum(['string']), + isComplete: z.boolean(), + value: z.string(), +}); + +const BooleanFragmentSchema = z.object({ + type: z.enum(['boolean']), + isComplete: z.boolean(), + value: z.boolean(), +}); + +export const BazelDepRecord = z + .object({ + type: z.enum(['record']), + isComplete: z.boolean(), + children: z.object({ + rule: z.object({ + type: z.enum(['string']), + isComplete: z.boolean(), + value: z.enum(['bazel_dep']), + }), + name: StringFragmentSchema, + version: StringFragmentSchema, + dev_dependency: BooleanFragmentSchema.optional(), + }), + }) + .transform((frag): RecordFragment => { + return Fragments.asRecord(frag); + }); + +export const BazelDepRecordToPackageDependency = BazelDepRecord.transform( + ({ children: { rule, name, version } }): PackageDependency => ({ + datasource: BazelDatasource.id, + depType: Fragments.asString(rule).value, + depName: Fragments.asString(name).value, + currentValue: Fragments.asString(version).value, + }) +); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 812515e25b4dee..4b415f1f99435c 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,8 +1,8 @@ import type { PackageFileContent } from '../types'; +import { BazelDepRecordToPackageDependency } from './bazel-dep'; import { instanceExists } from './filters'; import { RecordFragment } from './fragments'; import { parse } from './parser'; -import { BazelDepRecordToPackageDependency } from './rules'; export function extractPackageFile( content: string, @@ -12,14 +12,44 @@ export function extractPackageFile( if (!fragments) { return null; } + // DEBUG BEGIN + const dbgMsgs = []; + dbgMsgs.push('fragments: ', fragments, '\n'); + // DEBUG END const deps = fragments + // DEBUG BEGIN + .map((value) => { + dbgMsgs.push('value: ', value, '\n'); + const isRecord = value instanceof RecordFragment; + dbgMsgs.push('isRecord: ', isRecord, '\n'); + return value; + }) + // DEBUG END .filter((value) => value instanceof RecordFragment) .map((value) => value as RecordFragment) + // // DEBUG BEGIN + // .map((record) => { + // dbgMsgs.push('record: ', record, '\n'); + // return record; + // }) + // // DEBUG END .filter((record) => record.isRule('bazel_dep')) + // // DEBUG BEGIN + // .map((record) => { + // dbgMsgs.push('record: ', record, '\n'); + // return record; + // }) + // // DEBUG END .map((record) => { const result = BazelDepRecordToPackageDependency.safeParse(record); + // DEBUG BEGIN + dbgMsgs.push('result: ', result, '\n'); + // DEBUG END return result.success ? result.data : undefined; }) .filter(instanceExists); + // // DEBUG BEGIN + // console.log('*** CHUCK \n', ...dbgMsgs); + // // DEBUG END return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 4b82d584d33267..d2996e27a23b31 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -147,18 +147,27 @@ export class Fragments { } static asBoolean(frag: FragmentCompatible): BooleanFragment { + if (frag instanceof BooleanFragment) { + return frag; + } Fragments.checkType('boolean', frag.type); Object.setPrototypeOf(frag, BooleanFragment.prototype); return frag as BooleanFragment; } static asString(frag: FragmentCompatible): StringFragment { + if (frag instanceof StringFragment) { + return frag; + } Fragments.checkType('string', frag.type); Object.setPrototypeOf(frag, StringFragment.prototype); return frag as StringFragment; } static asArray(frag: FragmentCompatible): ArrayFragment { + if (frag instanceof ArrayFragment) { + return frag; + } Fragments.checkType('array', frag.type); Object.setPrototypeOf(frag, ArrayFragment.prototype); const array = frag as ArrayFragment; @@ -169,6 +178,9 @@ export class Fragments { } static asRecord(frag: FragmentCompatible): RecordFragment { + if (frag instanceof RecordFragment) { + return frag; + } Fragments.checkType('record', frag.type); Object.setPrototypeOf(frag, RecordFragment.prototype); const record = frag as RecordFragment; @@ -182,6 +194,9 @@ export class Fragments { static safeAsAttribute( frag: FragmentCompatible ): AttributeFragment | undefined { + if (frag instanceof AttributeFragment) { + return frag; + } if (frag.type !== 'attribute') { return undefined; } diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 392318cec6fb15..206c740d37bb5e 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -3,10 +3,11 @@ import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { Ctx } from './context'; import type { ValueFragment } from './fragments'; -import { supportedRulesRegex } from './rules'; import { StarlarkBoolean } from './starlark'; const booleanValuesRegex = regEx(`^${StarlarkBoolean.stringValues.join('|')}$`); +const supportedRules = ['bazel_dep']; +const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); /** * Matches key-value pairs: diff --git a/lib/modules/manager/bazel-module/rules/bazel-dep.ts b/lib/modules/manager/bazel-module/rules/bazel-dep.ts deleted file mode 100644 index 332475482c675d..00000000000000 --- a/lib/modules/manager/bazel-module/rules/bazel-dep.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { z } from 'zod'; -import { BazelDatasource } from '../../../datasource/bazel'; -import type { PackageDependency } from '../../types'; - -const StringFragmentSchema = z.object({ - type: z.enum(['string']), - isComplete: z.boolean(), - value: z.string(), -}); - -const BooleanFragmentSchema = z.object({ - type: z.enum(['boolean']), - isComplete: z.boolean(), - value: z.boolean(), -}); - -export const BazelDepRecord = z.object({ - type: z.enum(['record']), - isComplete: z.boolean(), - children: z.object({ - rule: z.object({ - type: z.enum(['string']), - isComplete: z.boolean(), - value: z.enum(['bazel_dep']), - }), - name: StringFragmentSchema, - version: StringFragmentSchema, - dev_dependency: BooleanFragmentSchema.optional(), - }), -}); - -export const BazelDepRecordToPackageDependency = BazelDepRecord.transform( - ({ children: { rule, name, version } }): PackageDependency => { - const dep: PackageDependency = { - datasource: BazelDatasource.id, - depType: rule.value, - depName: name.value, - currentValue: version.value, - }; - return dep; - } -); diff --git a/lib/modules/manager/bazel-module/rules/index.spec.ts b/lib/modules/manager/bazel-module/rules/index.spec.ts deleted file mode 100644 index 21b8d655a13407..00000000000000 --- a/lib/modules/manager/bazel-module/rules/index.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -// import { extractDepsFromFragment } from '.'; - -describe('modules/manager/bazel-module/rules/index', () => { - it.todo('IMPLEMENT ME!'); - // it.each` - // a | exp - // ${'bazel_dep(name = "rules_foo", version = "1.2.3")'} | ${[{ rule: 'bazel_dep', name: 'rules_foo', version: '1.2.3' }]} - // `('extractDepsFromFragmentData($a)', ({ a, exp }) => { - // const fragments = parse(a, 'MODULE.bazel'); - // if (!fragments) { - // throw new Error('Expected fragments not to be null.'); - // } - // expect(fragments).toHaveLength(1); - // expect(extractDepsFromFragment(fragments[0])).toEqual(exp); - // }); -}); diff --git a/lib/modules/manager/bazel-module/rules/index.ts b/lib/modules/manager/bazel-module/rules/index.ts deleted file mode 100644 index a00fb9f3b1b8a5..00000000000000 --- a/lib/modules/manager/bazel-module/rules/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { regEx } from '../../../../util/regex'; -import { BazelDepRecord, BazelDepRecordToPackageDependency } from './bazel-dep'; - -const supportedRules = ['bazel_dep']; -export const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); - -export { BazelDepRecord, BazelDepRecordToPackageDependency }; From 93e30db8d7c8d41381744ca1bdb1c57f8c722d96 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 17:25:24 -0600 Subject: [PATCH 20/80] Tests are green --- lib/modules/manager/bazel-module/extract.ts | 30 --------------------- lib/modules/manager/bazel-module/parser.ts | 3 ++- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 4b415f1f99435c..6b739fceb7ce53 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -12,44 +12,14 @@ export function extractPackageFile( if (!fragments) { return null; } - // DEBUG BEGIN - const dbgMsgs = []; - dbgMsgs.push('fragments: ', fragments, '\n'); - // DEBUG END const deps = fragments - // DEBUG BEGIN - .map((value) => { - dbgMsgs.push('value: ', value, '\n'); - const isRecord = value instanceof RecordFragment; - dbgMsgs.push('isRecord: ', isRecord, '\n'); - return value; - }) - // DEBUG END .filter((value) => value instanceof RecordFragment) .map((value) => value as RecordFragment) - // // DEBUG BEGIN - // .map((record) => { - // dbgMsgs.push('record: ', record, '\n'); - // return record; - // }) - // // DEBUG END .filter((record) => record.isRule('bazel_dep')) - // // DEBUG BEGIN - // .map((record) => { - // dbgMsgs.push('record: ', record, '\n'); - // return record; - // }) - // // DEBUG END .map((record) => { const result = BazelDepRecordToPackageDependency.safeParse(record); - // DEBUG BEGIN - dbgMsgs.push('result: ', result, '\n'); - // DEBUG END return result.success ? result.data : undefined; }) .filter(instanceExists); - // // DEBUG BEGIN - // console.log('*** CHUCK \n', ...dbgMsgs); - // // DEBUG END return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 206c740d37bb5e..e1bc9e79377534 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -73,7 +73,8 @@ export function parse( try { const parsedResult = starlark.query(input, query, new Ctx()); if (parsedResult) { - result = parsedResult.results; + // The parsedResult and its associated objects are missing their types. + result = Ctx.from(parsedResult).results; } } catch (err) /* istanbul ignore next */ { logger.debug({ err, packageFile }, 'Bazel module parsing error'); From 0edc936907e807392a4a75d6cf88e87a1d39fb57 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 27 Apr 2023 17:30:24 -0600 Subject: [PATCH 21/80] Do not export BazelDepRecord --- .../manager/bazel-module/bazel-dep.spec.ts | 24 +------------------ lib/modules/manager/bazel-module/bazel-dep.ts | 2 +- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts index 76e60a8ab76b84..44d180d9b76795 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -1,30 +1,8 @@ import { BazelDatasource } from '../../datasource/bazel'; -import { BazelDepRecord, BazelDepRecordToPackageDependency } from './bazel-dep'; +import { BazelDepRecordToPackageDependency } from './bazel-dep'; import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; describe('modules/manager/bazel-module/bazel-dep', () => { - describe('BazelDepFragment', () => { - it('parses record fragment without dev_dependency', () => { - const record = new RecordFragment({ - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), - }); - expect(() => BazelDepRecord.parse(record)).not.toThrow(); - }); - - it('parses record fragment with dev_dependency', () => { - const record = new RecordFragment({ - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), - dev_dependency: new BooleanFragment(true), - }); - const result = BazelDepRecord.parse(record); - expect(result).toBeInstanceOf(RecordFragment); - }); - }); - describe('BazelDepRecordToPackageDependency', () => { it('transforms a record fragment', () => { const record = new RecordFragment({ diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index d1573f86cb1770..438f3dd7776974 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -17,7 +17,7 @@ const BooleanFragmentSchema = z.object({ value: z.boolean(), }); -export const BazelDepRecord = z +const BazelDepRecord = z .object({ type: z.enum(['record']), isComplete: z.boolean(), From d0a91f263cb083f88e85ec88e8f2e6661748dae8 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 28 Apr 2023 08:26:44 -0600 Subject: [PATCH 22/80] Refactor extract. Add Fragments.safeAsAttribute. --- .../manager/bazel-module/extract.spec.ts | 6 ++-- lib/modules/manager/bazel-module/extract.ts | 13 +++---- .../manager/bazel-module/fragments.spec.ts | 36 +++++++++++++++++++ lib/modules/manager/bazel-module/fragments.ts | 14 ++++++-- lib/modules/manager/bazel-module/parser.ts | 12 +------ 5 files changed, 57 insertions(+), 24 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index cc67e68588c8f7..2fc65da42ef2d8 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -4,9 +4,9 @@ import { extractPackageFile } from '.'; describe('modules/manager/bazel-module/extract', () => { describe('extractPackageFile()', () => { - it('returns empty if fails to parse', () => { - const res = extractPackageFile('blahhhhh:foo:@what\n', 'MODULE.bazel'); - expect(res).toBeNull(); + it('returns null if fails to parse', () => { + const result = extractPackageFile('blahhhhh:foo:@what\n', 'MODULE.bazel'); + expect(result).toBeNull(); }); it('returns dependencies', () => { diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 6b739fceb7ce53..001676c94a5072 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,7 +1,7 @@ import type { PackageFileContent } from '../types'; import { BazelDepRecordToPackageDependency } from './bazel-dep'; import { instanceExists } from './filters'; -import { RecordFragment } from './fragments'; +import { Fragments } from './fragments'; import { parse } from './parser'; export function extractPackageFile( @@ -13,13 +13,10 @@ export function extractPackageFile( return null; } const deps = fragments - .filter((value) => value instanceof RecordFragment) - .map((value) => value as RecordFragment) + .map((value) => Fragments.safeAsRecord(value)) + .filter(instanceExists) .filter((record) => record.isRule('bazel_dep')) - .map((record) => { - const result = BazelDepRecordToPackageDependency.safeParse(record); - return result.success ? result.data : undefined; - }) - .filter(instanceExists); + .map((record) => BazelDepRecordToPackageDependency.parse(record)); + // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 80d465dc082cd9..cd14cc0de6799b 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -2,9 +2,11 @@ import { ArrayFragment, AttributeFragment, BooleanFragment, + FragmentType, Fragments, RecordFragment, StringFragment, + ValueFragment, } from './fragments'; describe('modules/manager/bazel-module/fragments', () => { @@ -125,6 +127,40 @@ describe('modules/manager/bazel-module/fragments', () => { }); }); + describe('asArray', () => { + it('returns an ArrayFragment', () => { + const frag: { + type: FragmentType; + isComplete: boolean; + items: ValueFragment[]; + } = { + type: 'array', + isComplete: false, + items: [new StringFragment('hello')], + }; + const result = Fragments.asArray(frag); + expect(result).toEqual(frag); + expect(result).toBeInstanceOf(ArrayFragment); + }); + }); + + describe('asAttribute', () => { + it('returns an AttributeFragment', () => { + const frag: { + type: FragmentType; + isComplete: boolean; + value?: ValueFragment; + } = { + type: 'attribute', + isComplete: false, + value: new StringFragment('hello'), + }; + const result = Fragments.asAttribute(frag); + expect(result).toEqual(frag); + expect(result).toBeInstanceOf(AttributeFragment); + }); + }); + it.each` frag | exp ${new StringFragment('hello')} | ${new StringFragment('hello')} diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index d2996e27a23b31..2006fe5ad1b684 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -177,11 +177,13 @@ export class Fragments { return array; } - static asRecord(frag: FragmentCompatible): RecordFragment { + static safeAsRecord(frag: FragmentCompatible): RecordFragment | undefined { if (frag instanceof RecordFragment) { return frag; } - Fragments.checkType('record', frag.type); + if (frag.type !== 'record') { + return undefined; + } Object.setPrototypeOf(frag, RecordFragment.prototype); const record = frag as RecordFragment; for (const prop in record.children) { @@ -191,6 +193,14 @@ export class Fragments { return record; } + static asRecord(frag: FragmentCompatible): RecordFragment { + const record = Fragments.safeAsRecord(frag); + if (record) { + return record; + } + throw this.typeError('record', frag.type); + } + static safeAsAttribute( frag: FragmentCompatible ): AttributeFragment | undefined { diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index e1bc9e79377534..15dd3773a63690 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -11,18 +11,8 @@ const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); /** * Matches key-value pairs: - * - `tag = "1.2.3"` * - `name = "foobar"` - * - `deps = ["foo", "bar"]` - * - ` - * artifacts = [ - maven.artifact( - group = "com.example1", - artifact = "foobar", - version = "1.2.3", - ) - ] - ` + * - `dev_dependeny = True` **/ const kwParams = q .sym((ctx, token) => { From 36b64227b7660373b117464c107343601425cd81 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 28 Apr 2023 10:28:33 -0600 Subject: [PATCH 23/80] Add schemas for fragments --- lib/modules/manager/bazel-module/fragments.ts | 75 ++++++++++++++++--- 1 file changed, 63 insertions(+), 12 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 2006fe5ad1b684..05bbab16536ae8 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -1,6 +1,54 @@ import is from '@sindresorhus/is'; +import { z } from 'zod'; import { StarlarkBoolean } from './starlark'; +// Fragment Schemas + +const FragmentTypeSchema = z.enum([ + 'string', + 'boolean', + 'array', + 'record', + 'attribute', +]); +const CommonFragmentSchema = z.object({ + type: FragmentTypeSchema, +}); +const CompletableSchema = z.object({ + isComplete: z.boolean(), +}); +const ChildFragmentsSchema = z.record(z.string(), CommonFragmentSchema); +const StringFragmentSchema = CommonFragmentSchema.merge( + CompletableSchema +).extend({ + type: z.enum(['string']), + value: z.string(), +}); +const BooleanFragmentSchema = CommonFragmentSchema.merge( + CompletableSchema +).extend({ + type: z.enum(['boolean']), + value: z.boolean(), +}); +const ArrayFragmentSchema = CommonFragmentSchema.merge( + CompletableSchema +).extend({ + type: z.enum(['array']), + items: z.array(CommonFragmentSchema), +}); +const RecordFragmentSchema = CommonFragmentSchema.merge( + CompletableSchema +).extend({ + type: z.enum(['record']), + children: ChildFragmentsSchema, +}); +const AttributeFragmentSchema = CommonFragmentSchema.extend({ + type: z.enum(['attribute']), + value: CommonFragmentSchema.optional(), +}); + +// Fragment Types + export type FragmentType = | 'string' | 'boolean' @@ -13,12 +61,14 @@ export interface FragmentCompatible { } export class StringFragment implements FragmentCompatible { + static readonly schema = StringFragmentSchema; readonly type: FragmentType = 'string'; readonly isComplete = true; constructor(readonly value: string) {} } export class BooleanFragment implements FragmentCompatible { + static readonly schema = BooleanFragmentSchema; readonly type: FragmentType = 'boolean'; readonly isComplete = true; readonly value: boolean; @@ -29,6 +79,7 @@ export class BooleanFragment implements FragmentCompatible { } export class ArrayFragment implements FragmentCompatible { + static readonly schema = ArrayFragmentSchema; readonly type: FragmentType = 'array'; isComplete = false; items: ValueFragment[] = []; @@ -44,6 +95,7 @@ export class ArrayFragment implements FragmentCompatible { } export class RecordFragment implements FragmentCompatible { + static readonly schema = RecordFragmentSchema; readonly type: FragmentType = 'record'; isComplete = false; children: ChildFragments; @@ -73,6 +125,7 @@ export class RecordFragment implements FragmentCompatible { } export class AttributeFragment implements FragmentCompatible { + static readonly schema = AttributeFragmentSchema; readonly type: FragmentType = 'attribute'; readonly name: string; value?: ValueFragment; @@ -99,18 +152,13 @@ export type ValueFragment = export type ChildFragments = Record; export type Fragment = ValueFragment | AttributeFragment; +// Fragments Class + export class Fragments { private static typeError(expected: string, actual: string): Error { return new Error(`Expected type ${expected}, but was ${actual}.`); } - private static checkType(expected: string, actual: string): void { - if (expected === actual) { - return; - } - throw Fragments.typeError(expected, actual); - } - static safeAsValue(frag: FragmentCompatible): ValueFragment | undefined { switch (frag.type) { case 'string': @@ -150,7 +198,8 @@ export class Fragments { if (frag instanceof BooleanFragment) { return frag; } - Fragments.checkType('boolean', frag.type); + BooleanFragmentSchema.parse(frag); + // Fragments.checkType('boolean', frag.type); Object.setPrototypeOf(frag, BooleanFragment.prototype); return frag as BooleanFragment; } @@ -159,7 +208,7 @@ export class Fragments { if (frag instanceof StringFragment) { return frag; } - Fragments.checkType('string', frag.type); + StringFragmentSchema.parse(frag); Object.setPrototypeOf(frag, StringFragment.prototype); return frag as StringFragment; } @@ -168,7 +217,7 @@ export class Fragments { if (frag instanceof ArrayFragment) { return frag; } - Fragments.checkType('array', frag.type); + ArrayFragmentSchema.parse(frag); Object.setPrototypeOf(frag, ArrayFragment.prototype); const array = frag as ArrayFragment; for (let i = 0; i < array.items.length; i++) { @@ -181,7 +230,8 @@ export class Fragments { if (frag instanceof RecordFragment) { return frag; } - if (frag.type !== 'record') { + const parseResult = RecordFragmentSchema.safeParse(frag); + if (!parseResult.success) { return undefined; } Object.setPrototypeOf(frag, RecordFragment.prototype); @@ -207,7 +257,8 @@ export class Fragments { if (frag instanceof AttributeFragment) { return frag; } - if (frag.type !== 'attribute') { + const parseResult = AttributeFragmentSchema.safeParse(frag); + if (!parseResult.success) { return undefined; } Object.setPrototypeOf(frag, AttributeFragment.prototype); From 743a626674e50d78760a21719a0d60f1f6400e08 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 28 Apr 2023 10:46:24 -0600 Subject: [PATCH 24/80] Change extract to use zod parse/transform to filter output --- lib/modules/manager/bazel-module/bazel-dep.ts | 37 ++++++------------- lib/modules/manager/bazel-module/extract.ts | 10 ++--- .../manager/bazel-module/fragments.spec.ts | 12 ------ lib/modules/manager/bazel-module/fragments.ts | 11 ------ 4 files changed, 17 insertions(+), 53 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index 438f3dd7776974..1239ea460d4ec8 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -1,35 +1,22 @@ import { z } from 'zod'; import { BazelDatasource } from '../../datasource/bazel'; import type { PackageDependency } from '../types'; -import { Fragments, RecordFragment } from './fragments'; +import { + BooleanFragment, + Fragments, + RecordFragment, + StringFragment, +} from './fragments'; -// TODO: Move schema to static properties on the XXXFragment classes. - -const StringFragmentSchema = z.object({ - type: z.enum(['string']), - isComplete: z.boolean(), - value: z.string(), -}); - -const BooleanFragmentSchema = z.object({ - type: z.enum(['boolean']), - isComplete: z.boolean(), - value: z.boolean(), -}); - -const BazelDepRecord = z - .object({ - type: z.enum(['record']), - isComplete: z.boolean(), +const BazelDepRecord = RecordFragment.schema + .extend({ children: z.object({ - rule: z.object({ - type: z.enum(['string']), - isComplete: z.boolean(), + rule: StringFragment.schema.extend({ value: z.enum(['bazel_dep']), }), - name: StringFragmentSchema, - version: StringFragmentSchema, - dev_dependency: BooleanFragmentSchema.optional(), + name: StringFragment.schema, + version: StringFragment.schema, + dev_dependency: BooleanFragment.schema.optional(), }), }) .transform((frag): RecordFragment => { diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 001676c94a5072..43fa55153a2cf3 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,7 +1,6 @@ import type { PackageFileContent } from '../types'; import { BazelDepRecordToPackageDependency } from './bazel-dep'; import { instanceExists } from './filters'; -import { Fragments } from './fragments'; import { parse } from './parser'; export function extractPackageFile( @@ -13,10 +12,11 @@ export function extractPackageFile( return null; } const deps = fragments - .map((value) => Fragments.safeAsRecord(value)) - .filter(instanceExists) - .filter((record) => record.isRule('bazel_dep')) - .map((record) => BazelDepRecordToPackageDependency.parse(record)); + .map((record) => { + const result = BazelDepRecordToPackageDependency.safeParse(record); + return result.success ? result.data : undefined; + }) + .filter(instanceExists); // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index cd14cc0de6799b..29c3b257bfd078 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -72,18 +72,6 @@ describe('modules/manager/bazel-module/fragments', () => { expect(record.children).toEqual({ name: new StringFragment('chicken') }); expect(record.isComplete).toBe(false); }); - - it.each` - record | type | exp - ${new RecordFragment()} | ${undefined} | ${false} - ${new RecordFragment()} | ${'bazel_dep'} | ${false} - ${new RecordFragment({ rule: new StringFragment('bazel_dep') })} | ${undefined} | ${true} - ${new RecordFragment({ rule: new StringFragment('bazel_dep') })} | ${'bazel_dep'} | ${true} - ${new RecordFragment({ rule: new StringFragment('foo') })} | ${'bazel_dep'} | ${false} - ${new RecordFragment({ rule: new ArrayFragment() })} | ${'bazel_dep'} | ${false} - `('.isRule', ({ record, type, exp }) => { - expect(record.isRule(type)).toBe(exp); - }); }); describe('AttributeFragment', () => { diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 05bbab16536ae8..90898245268acc 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -111,17 +111,6 @@ export class RecordFragment implements FragmentCompatible { } this.children[attrib.name] = attrib.value; } - - isRule(type?: string): boolean { - let result = 'rule' in this.children; - if (type) { - const ruleValue = this.children['rule']; - const ruleValueStr = - ruleValue instanceof StringFragment ? ruleValue : undefined; - result = result && ruleValueStr?.value === type; - } - return result; - } } export class AttributeFragment implements FragmentCompatible { From 6c04e9b59a906b9db303be4c6f2e2982733423cc Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 28 Apr 2023 13:24:37 -0600 Subject: [PATCH 25/80] Fix typeError bug --- lib/modules/manager/bazel-module/extract.ts | 1 + lib/modules/manager/bazel-module/fragments.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 43fa55153a2cf3..f7173fda75ab96 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -14,6 +14,7 @@ export function extractPackageFile( const deps = fragments .map((record) => { const result = BazelDepRecordToPackageDependency.safeParse(record); + // istanbul ignore next: cannot reach undefined without an additional rule return result.success ? result.data : undefined; }) .filter(instanceExists); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 90898245268acc..b94963b709ba73 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -188,7 +188,6 @@ export class Fragments { return frag; } BooleanFragmentSchema.parse(frag); - // Fragments.checkType('boolean', frag.type); Object.setPrototypeOf(frag, BooleanFragment.prototype); return frag as BooleanFragment; } @@ -237,7 +236,7 @@ export class Fragments { if (record) { return record; } - throw this.typeError('record', frag.type); + throw Fragments.typeError('record', frag.type); } static safeAsAttribute( @@ -263,6 +262,6 @@ export class Fragments { if (attribute) { return attribute; } - throw this.typeError('attribute', frag.type); + throw Fragments.typeError('attribute', frag.type); } } From 49c2a76005689c78521586dd0dee7c317ab99289 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 28 Apr 2023 13:48:03 -0600 Subject: [PATCH 26/80] Add readme.md --- lib/modules/manager/bazel-module/readme.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 lib/modules/manager/bazel-module/readme.md diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md new file mode 100644 index 00000000000000..86f9e8abd0fe2b --- /dev/null +++ b/lib/modules/manager/bazel-module/readme.md @@ -0,0 +1,5 @@ +The `bazel-module` manager supports the maintenance of [Bazel module (bzlmod)] enabled workspaces. + + + +[Bazel module (bzlmod)]: https://bazel.build/external/module From 4af114322f1191b0462dbedefad215d24fbfa0eb Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 28 Apr 2023 14:05:11 -0600 Subject: [PATCH 27/80] Add test for invalid string to new BooleanFragment. --- lib/modules/manager/bazel-module/fragments.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 29c3b257bfd078..f20f1c97117918 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -36,7 +36,14 @@ describe('modules/manager/bazel-module/fragments', () => { expect(bool.value).toBe(exp); }); - it.todo('Add test for invalid string'); + it.each` + a + ${'true'} + ${'false'} + ${''} + `('new BooleanFragment($a)', ({ a }) => { + expect(() => new BooleanFragment(a)).toThrow(); + }); }); describe('ArrayFragment', () => { From 7908cfcf452eb67275c06b494e324369b2cfbd9e Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Sat, 29 Apr 2023 09:08:43 -0600 Subject: [PATCH 28/80] Remove TODO. --- lib/modules/manager/bazel-module/parser.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 15dd3773a63690..590998b2715807 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -57,8 +57,6 @@ export function parse( input: string, packageFile?: string ): ValueFragment[] | null { - // TODO: Add the mem cache. - let result: ValueFragment[] | null = null; try { const parsedResult = starlark.query(input, query, new Ctx()); @@ -69,6 +67,5 @@ export function parse( } catch (err) /* istanbul ignore next */ { logger.debug({ err, packageFile }, 'Bazel module parsing error'); } - return result; } From 024e2a34c1d17179b200fc5afbc4c5de4eaf1119 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Sat, 29 Apr 2023 11:10:17 -0600 Subject: [PATCH 29/80] Move as functions to their respective class. --- lib/modules/manager/bazel-module/bazel-dep.ts | 15 +- lib/modules/manager/bazel-module/context.ts | 4 +- .../manager/bazel-module/fragments.spec.ts | 78 ++++---- lib/modules/manager/bazel-module/fragments.ts | 179 +++++++++--------- 4 files changed, 137 insertions(+), 139 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index 1239ea460d4ec8..81774e83624a5a 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -1,12 +1,7 @@ import { z } from 'zod'; import { BazelDatasource } from '../../datasource/bazel'; import type { PackageDependency } from '../types'; -import { - BooleanFragment, - Fragments, - RecordFragment, - StringFragment, -} from './fragments'; +import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; const BazelDepRecord = RecordFragment.schema .extend({ @@ -20,14 +15,14 @@ const BazelDepRecord = RecordFragment.schema }), }) .transform((frag): RecordFragment => { - return Fragments.asRecord(frag); + return RecordFragment.as(frag); }); export const BazelDepRecordToPackageDependency = BazelDepRecord.transform( ({ children: { rule, name, version } }): PackageDependency => ({ datasource: BazelDatasource.id, - depType: Fragments.asString(rule).value, - depName: Fragments.asString(name).value, - currentValue: Fragments.asString(version).value, + depType: StringFragment.as(rule).value, + depName: StringFragment.as(name).value, + currentValue: StringFragment.as(version).value, }) ); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index ee077f3f70a4ad..e8b8ea1fc55608 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -29,7 +29,7 @@ export class Ctx implements CtxCompatible { const ctx = obj as Ctx; const stackItems = ctx.stack.map((item) => Fragments.asFragment(item)); ctx.stack = Stack.create(...stackItems); - ctx.results = ctx.results.map((item) => Fragments.asRecord(item)); + ctx.results = ctx.results.map((item) => RecordFragment.as(item)); return ctx; } @@ -56,7 +56,7 @@ export class Ctx implements CtxCompatible { return; } const value = Fragments.safeAsValue(current); - const attribute = Fragments.safeAsAttribute(current); + const attribute = AttributeFragment.safeAs(current); const newCurrent = this.stack.safeCurrent; if (value) { if (newCurrent && 'addValue' in newCurrent) { diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index f20f1c97117918..df492505a52da5 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -56,6 +56,23 @@ describe('modules/manager/bazel-module/fragments', () => { expect(array.items).toEqual([new StringFragment('hello')]); expect(array.isComplete).toBe(false); }); + + describe('as', () => { + it('returns an ArrayFragment', () => { + const frag: { + type: FragmentType; + isComplete: boolean; + items: ValueFragment[]; + } = { + type: 'array', + isComplete: false, + items: [new StringFragment('hello')], + }; + const result = ArrayFragment.as(frag); + expect(result).toEqual(frag); + expect(result).toBeInstanceOf(ArrayFragment); + }); + }); }); describe('RecordFragment', () => { @@ -93,6 +110,23 @@ describe('modules/manager/bazel-module/fragments', () => { expect(attribute.value).toEqual(value); expect(attribute.isComplete).toBe(true); }); + + describe('as', () => { + it('returns an AttributeFragment', () => { + const frag: { + type: FragmentType; + isComplete: boolean; + value?: ValueFragment; + } = { + type: 'attribute', + isComplete: false, + value: new StringFragment('hello'), + }; + const result = AttributeFragment.as(frag); + expect(result).toEqual(frag); + expect(result).toBeInstanceOf(AttributeFragment); + }); + }); }); describe('Fragments', () => { @@ -122,40 +156,6 @@ describe('modules/manager/bazel-module/fragments', () => { }); }); - describe('asArray', () => { - it('returns an ArrayFragment', () => { - const frag: { - type: FragmentType; - isComplete: boolean; - items: ValueFragment[]; - } = { - type: 'array', - isComplete: false, - items: [new StringFragment('hello')], - }; - const result = Fragments.asArray(frag); - expect(result).toEqual(frag); - expect(result).toBeInstanceOf(ArrayFragment); - }); - }); - - describe('asAttribute', () => { - it('returns an AttributeFragment', () => { - const frag: { - type: FragmentType; - isComplete: boolean; - value?: ValueFragment; - } = { - type: 'attribute', - isComplete: false, - value: new StringFragment('hello'), - }; - const result = Fragments.asAttribute(frag); - expect(result).toEqual(frag); - expect(result).toBeInstanceOf(AttributeFragment); - }); - }); - it.each` frag | exp ${new StringFragment('hello')} | ${new StringFragment('hello')} @@ -168,11 +168,11 @@ describe('modules/manager/bazel-module/fragments', () => { }); it.each` - fn | frag - ${Fragments.asString} | ${new ArrayFragment()} - ${Fragments.asArray} | ${new RecordFragment()} - ${Fragments.asRecord} | ${new ArrayFragment()} - ${Fragments.asAttribute} | ${new ArrayFragment()} + fn | frag + ${StringFragment.as} | ${new ArrayFragment()} + ${ArrayFragment.as} | ${new RecordFragment()} + ${RecordFragment.as} | ${new ArrayFragment()} + ${AttributeFragment.as} | ${new ArrayFragment()} `('$fn throws with $frag', ({ fn, frag }) => { expect(() => fn(frag)).toThrow(); }); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index b94963b709ba73..5eb51d1ea8a78a 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -62,6 +62,16 @@ export interface FragmentCompatible { export class StringFragment implements FragmentCompatible { static readonly schema = StringFragmentSchema; + + static as(frag: FragmentCompatible): StringFragment { + if (frag instanceof StringFragment) { + return frag; + } + StringFragmentSchema.parse(frag); + Object.setPrototypeOf(frag, StringFragment.prototype); + return frag as StringFragment; + } + readonly type: FragmentType = 'string'; readonly isComplete = true; constructor(readonly value: string) {} @@ -69,6 +79,16 @@ export class StringFragment implements FragmentCompatible { export class BooleanFragment implements FragmentCompatible { static readonly schema = BooleanFragmentSchema; + + static as(frag: FragmentCompatible): BooleanFragment { + if (frag instanceof BooleanFragment) { + return frag; + } + BooleanFragmentSchema.parse(frag); + Object.setPrototypeOf(frag, BooleanFragment.prototype); + return frag as BooleanFragment; + } + readonly type: FragmentType = 'boolean'; readonly isComplete = true; readonly value: boolean; @@ -80,6 +100,20 @@ export class BooleanFragment implements FragmentCompatible { export class ArrayFragment implements FragmentCompatible { static readonly schema = ArrayFragmentSchema; + + static as(frag: FragmentCompatible): ArrayFragment { + if (frag instanceof ArrayFragment) { + return frag; + } + ArrayFragmentSchema.parse(frag); + Object.setPrototypeOf(frag, ArrayFragment.prototype); + const array = frag as ArrayFragment; + for (let i = 0; i < array.items.length; i++) { + array.items[i] = Fragments.asValue(array.items[i]); + } + return array; + } + readonly type: FragmentType = 'array'; isComplete = false; items: ValueFragment[] = []; @@ -96,6 +130,32 @@ export class ArrayFragment implements FragmentCompatible { export class RecordFragment implements FragmentCompatible { static readonly schema = RecordFragmentSchema; + + static safeAs(frag: FragmentCompatible): RecordFragment | undefined { + if (frag instanceof RecordFragment) { + return frag; + } + const parseResult = RecordFragmentSchema.safeParse(frag); + if (!parseResult.success) { + return undefined; + } + Object.setPrototypeOf(frag, RecordFragment.prototype); + const record = frag as RecordFragment; + for (const prop in record.children) { + const child = record.children[prop]; + record.children[prop] = Fragments.asValue(child); + } + return record; + } + + static as(frag: FragmentCompatible): RecordFragment { + const record = RecordFragment.safeAs(frag); + if (record) { + return record; + } + throw Fragments.typeError('record', frag.type); + } + readonly type: FragmentType = 'record'; isComplete = false; children: ChildFragments; @@ -115,6 +175,31 @@ export class RecordFragment implements FragmentCompatible { export class AttributeFragment implements FragmentCompatible { static readonly schema = AttributeFragmentSchema; + + static safeAs(frag: FragmentCompatible): AttributeFragment | undefined { + if (frag instanceof AttributeFragment) { + return frag; + } + const parseResult = AttributeFragmentSchema.safeParse(frag); + if (!parseResult.success) { + return undefined; + } + Object.setPrototypeOf(frag, AttributeFragment.prototype); + const attribute = frag as AttributeFragment; + if (attribute.value) { + attribute.value = Fragments.asValue(attribute.value); + } + return attribute; + } + + static as(frag: FragmentCompatible): AttributeFragment { + const attribute = AttributeFragment.safeAs(frag); + if (attribute) { + return attribute; + } + throw Fragments.typeError('attribute', frag.type); + } + readonly type: FragmentType = 'attribute'; readonly name: string; value?: ValueFragment; @@ -144,20 +229,20 @@ export type Fragment = ValueFragment | AttributeFragment; // Fragments Class export class Fragments { - private static typeError(expected: string, actual: string): Error { + static typeError(expected: string, actual: string): Error { return new Error(`Expected type ${expected}, but was ${actual}.`); } static safeAsValue(frag: FragmentCompatible): ValueFragment | undefined { switch (frag.type) { case 'string': - return Fragments.asString(frag); + return StringFragment.as(frag); case 'boolean': - return Fragments.asBoolean(frag); + return BooleanFragment.as(frag); case 'array': - return Fragments.asArray(frag); + return ArrayFragment.as(frag); case 'record': - return Fragments.asRecord(frag); + return RecordFragment.as(frag); default: return undefined; } @@ -177,91 +262,9 @@ export class Fragments { return value; } if (frag.type === 'attribute') { - return Fragments.asAttribute(frag); + return AttributeFragment.as(frag); } // istanbul ignore next: can only get here if new type addded, but no impl throw new Error(`Unexpected fragment type: ${frag.type}`); } - - static asBoolean(frag: FragmentCompatible): BooleanFragment { - if (frag instanceof BooleanFragment) { - return frag; - } - BooleanFragmentSchema.parse(frag); - Object.setPrototypeOf(frag, BooleanFragment.prototype); - return frag as BooleanFragment; - } - - static asString(frag: FragmentCompatible): StringFragment { - if (frag instanceof StringFragment) { - return frag; - } - StringFragmentSchema.parse(frag); - Object.setPrototypeOf(frag, StringFragment.prototype); - return frag as StringFragment; - } - - static asArray(frag: FragmentCompatible): ArrayFragment { - if (frag instanceof ArrayFragment) { - return frag; - } - ArrayFragmentSchema.parse(frag); - Object.setPrototypeOf(frag, ArrayFragment.prototype); - const array = frag as ArrayFragment; - for (let i = 0; i < array.items.length; i++) { - array.items[i] = Fragments.asValue(array.items[i]); - } - return array; - } - - static safeAsRecord(frag: FragmentCompatible): RecordFragment | undefined { - if (frag instanceof RecordFragment) { - return frag; - } - const parseResult = RecordFragmentSchema.safeParse(frag); - if (!parseResult.success) { - return undefined; - } - Object.setPrototypeOf(frag, RecordFragment.prototype); - const record = frag as RecordFragment; - for (const prop in record.children) { - const child = record.children[prop]; - record.children[prop] = Fragments.asValue(child); - } - return record; - } - - static asRecord(frag: FragmentCompatible): RecordFragment { - const record = Fragments.safeAsRecord(frag); - if (record) { - return record; - } - throw Fragments.typeError('record', frag.type); - } - - static safeAsAttribute( - frag: FragmentCompatible - ): AttributeFragment | undefined { - if (frag instanceof AttributeFragment) { - return frag; - } - const parseResult = AttributeFragmentSchema.safeParse(frag); - if (!parseResult.success) { - return undefined; - } - Object.setPrototypeOf(frag, AttributeFragment.prototype); - const attribute = frag as AttributeFragment; - if (attribute.value) { - attribute.value = Fragments.asValue(attribute.value); - } - return attribute; - } - - static asAttribute(frag: FragmentCompatible): AttributeFragment { - const attribute = Fragments.safeAsAttribute(frag); - if (attribute) { - return attribute; - } - throw Fragments.typeError('attribute', frag.type); - } } From f4dcc59f89a3980d31502e5be9784751eeef68a4 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Sat, 29 Apr 2023 11:16:27 -0600 Subject: [PATCH 30/80] Refactor popStack --- lib/modules/manager/bazel-module/context.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index e8b8ea1fc55608..71700064bd56f8 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -55,24 +55,25 @@ export class Ctx implements CtxCompatible { if (!current) { return; } + const parent = this.stack.safeCurrent; const value = Fragments.safeAsValue(current); - const attribute = AttributeFragment.safeAs(current); - const newCurrent = this.stack.safeCurrent; if (value) { - if (newCurrent && 'addValue' in newCurrent) { - newCurrent.addValue(value); + if (parent && 'addValue' in parent) { + parent.addValue(value); return; } - if (!newCurrent) { + if (!parent) { this.results.push(value); return; } - } else if (attribute) { - if (newCurrent && 'addAttribute' in newCurrent) { - newCurrent.addAttribute(attribute); + } + const attribute = AttributeFragment.safeAs(current); + if (attribute) { + if (parent && 'addAttribute' in parent) { + parent.addAttribute(attribute); return; } - if (!newCurrent) { + if (!parent) { throw new Error('Processing an attribute but there is no parent.'); } } From 5ad60794d2236f6a701d3f8e21161561b2a15ae7 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Sat, 29 Apr 2023 11:21:51 -0600 Subject: [PATCH 31/80] Rename Ctx.from to Ctx.as. --- lib/modules/manager/bazel-module/context.spec.ts | 4 ++-- lib/modules/manager/bazel-module/context.ts | 2 +- lib/modules/manager/bazel-module/parser.ts | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index 3a46d293e40ac5..745240d7aebfe9 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -94,7 +94,7 @@ describe('modules/manager/bazel-module/context', () => { }); }); - describe('Ctx.from', () => { + describe('Ctx.as', () => { it('adds the appropriate prototype to the context and referenced fragments', () => { // Ensure that we have values in results (bazel_dep) and the stack (foo_library). const ctx = new Ctx() @@ -105,7 +105,7 @@ describe('modules/manager/bazel-module/context', () => { .addString('1.2.3') .endRule() .startRule('foo_library'); - const result = Ctx.from(ctx); + const result = Ctx.as(ctx); expect(result).toEqual(ctx); }); }); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 71700064bd56f8..1f1f1947863a81 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -24,7 +24,7 @@ export class Ctx implements CtxCompatible { // This exists because the good-enough-parser gives a cloned instance of our // Ctx. It is missing the Ctx prototype. This function adds the proper prototype // to the context and the items referenced by the context. - static from(obj: CtxCompatible): Ctx { + static as(obj: CtxCompatible): Ctx { Object.setPrototypeOf(obj, Ctx.prototype); const ctx = obj as Ctx; const stackItems = ctx.stack.map((item) => Fragments.asFragment(item)); diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 590998b2715807..f24d6086211a26 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -16,21 +16,21 @@ const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); **/ const kwParams = q .sym((ctx, token) => { - return Ctx.from(ctx).startAttribute(token.value); + return Ctx.as(ctx).startAttribute(token.value); }) .op('=') .alt( q.str((ctx, token) => { - return Ctx.from(ctx).addString(token.value); + return Ctx.as(ctx).addString(token.value); }), q.sym(booleanValuesRegex, (ctx, token) => { - return Ctx.from(ctx).addBoolean(token.value); + return Ctx.as(ctx).addBoolean(token.value); }) ); const moduleRules = q .sym(supportedRulesRegex, (ctx, token) => { - return Ctx.from(ctx).startRule(token.value); + return Ctx.as(ctx).startRule(token.value); }) .join( q.tree({ @@ -38,7 +38,7 @@ const moduleRules = q maxDepth: 1, search: kwParams, postHandler: (ctx, tree) => { - return Ctx.from(ctx).endRule(); + return Ctx.as(ctx).endRule(); }, }) ); @@ -62,7 +62,7 @@ export function parse( const parsedResult = starlark.query(input, query, new Ctx()); if (parsedResult) { // The parsedResult and its associated objects are missing their types. - result = Ctx.from(parsedResult).results; + result = Ctx.as(parsedResult).results; } } catch (err) /* istanbul ignore next */ { logger.debug({ err, packageFile }, 'Bazel module parsing error'); From 70633c9dcbad197543509fcd17250ea7c13e467f Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Sun, 30 Apr 2023 08:32:47 -0600 Subject: [PATCH 32/80] Disable the manager --- lib/modules/manager/bazel-module/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/modules/manager/bazel-module/index.ts b/lib/modules/manager/bazel-module/index.ts index c1b65aab70180f..2f19585168c573 100644 --- a/lib/modules/manager/bazel-module/index.ts +++ b/lib/modules/manager/bazel-module/index.ts @@ -5,6 +5,10 @@ export { extractPackageFile }; export const defaultConfig = { fileMatch: ['(^|/)MODULE\\.bazel$'], + // The bazel-module manager is still under development. The milestone + // tracking the release of this manager is at + // https://github.com/cgrindel/renovate_bzlmod_support/milestone/2. + enabled: false, }; export const supportedDatasources = [BazelDatasource.id]; From cf883b4e1ac3b6549b149f203369bfe7382295d5 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 2 May 2023 07:54:52 -0600 Subject: [PATCH 33/80] Update WIP comment with link to renovate tracking issue. --- lib/modules/manager/bazel-module/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/index.ts b/lib/modules/manager/bazel-module/index.ts index 2f19585168c573..3312b93fe6573d 100644 --- a/lib/modules/manager/bazel-module/index.ts +++ b/lib/modules/manager/bazel-module/index.ts @@ -7,7 +7,7 @@ export const defaultConfig = { fileMatch: ['(^|/)MODULE\\.bazel$'], // The bazel-module manager is still under development. The milestone // tracking the release of this manager is at - // https://github.com/cgrindel/renovate_bzlmod_support/milestone/2. + // https://github.com/renovatebot/renovate/issues/13658. enabled: false, }; From 6b1f68c2bf13adb676577f6e3b5192c7acb6d48b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 2 May 2023 07:57:28 -0600 Subject: [PATCH 34/80] Update the readme that the manager is WIP. --- lib/modules/manager/bazel-module/readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md index 86f9e8abd0fe2b..06ee7be393339d 100644 --- a/lib/modules/manager/bazel-module/readme.md +++ b/lib/modules/manager/bazel-module/readme.md @@ -1,5 +1,9 @@ +_NOTE: The `bazel-module` manager is currently a work-in-progress. For more information, please see +[the tracking issue]._ + The `bazel-module` manager supports the maintenance of [Bazel module (bzlmod)] enabled workspaces. [Bazel module (bzlmod)]: https://bazel.build/external/module +[the tracking issue]: https://github.com/renovatebot/renovate/issues/13658 From 4f6efd6d05cf1c800e4c2c2c4992f7a697841d7c Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 4 May 2023 14:39:15 -0600 Subject: [PATCH 35/80] Apply formatting to readme.md based upon review feedback. --- lib/modules/manager/bazel-module/readme.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md index 06ee7be393339d..1b61bcd1244c04 100644 --- a/lib/modules/manager/bazel-module/readme.md +++ b/lib/modules/manager/bazel-module/readme.md @@ -1,9 +1,7 @@ -_NOTE: The `bazel-module` manager is currently a work-in-progress. For more information, please see -[the tracking issue]._ + +!!! warning + The `bazel-module` manager is work-in-progress. + For more information, see [issue 13658](https://github.com/renovatebot/renovate/issues/13658). -The `bazel-module` manager supports the maintenance of [Bazel module (bzlmod)] enabled workspaces. - - - -[Bazel module (bzlmod)]: https://bazel.build/external/module -[the tracking issue]: https://github.com/renovatebot/renovate/issues/13658 +The `bazel-module` manager supports the maintenance of [Bazel module +(bzlmod)](https://bazel.build/external/module) enabled workspaces. From d40cb764323c77d08f90cdcb23208bdc36d681d1 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 4 May 2023 14:41:43 -0600 Subject: [PATCH 36/80] Reformat last line in readme.md. --- lib/modules/manager/bazel-module/readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md index 1b61bcd1244c04..3e15418c474e13 100644 --- a/lib/modules/manager/bazel-module/readme.md +++ b/lib/modules/manager/bazel-module/readme.md @@ -3,5 +3,4 @@ The `bazel-module` manager is work-in-progress. For more information, see [issue 13658](https://github.com/renovatebot/renovate/issues/13658). -The `bazel-module` manager supports the maintenance of [Bazel module -(bzlmod)](https://bazel.build/external/module) enabled workspaces. +The `bazel-module` manager supports the maintenance of [Bazel module (bzlmod)](https://bazel.build/external/module) enabled workspaces. From d5dc0cb13e44e5df7222ccefa22308a22d4f675d Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 4 May 2023 16:07:42 -0600 Subject: [PATCH 37/80] Remove it from test descriptions --- lib/modules/manager/bazel-module/context.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index 745240d7aebfe9..e817044a4309a3 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -64,7 +64,7 @@ describe('modules/manager/bazel-module/context', () => { expect(ctx.currentRecord).toEqual(new RecordFragment()); }); - it('throws if it the current is not a record fragment', () => { + it('throws if the current is not a record fragment', () => { const ctx = new Ctx().startArray(); expect(() => ctx.currentRecord).toThrow( new Error('Requested current record, but does not exist.') @@ -78,7 +78,7 @@ describe('modules/manager/bazel-module/context', () => { expect(ctx.currentArray).toEqual(new ArrayFragment()); }); - it('throws if it the current is not a record fragment', () => { + it('throws if the current is not a record fragment', () => { const ctx = new Ctx().startRecord(); expect(() => ctx.currentArray).toThrow( new Error('Requested current array, but does not exist.') From 95020783b1795518502507f453fa8380f5a10299 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 4 May 2023 16:08:48 -0600 Subject: [PATCH 38/80] Clarify test description about adding an attribute without a parent. --- lib/modules/manager/bazel-module/context.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index e817044a4309a3..384a4e9501ae71 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -86,7 +86,7 @@ describe('modules/manager/bazel-module/context', () => { }); }); - it('throws if add an attribute without a record', () => { + it('throws if add an attribute without a parent', () => { const ctx = new Ctx().startAttribute('name'); expect(() => ctx.addString('chicken')).toThrow( new Error('Processing an attribute but there is no parent.') From 5224ff104ce062a8c57250ba81c62cd34f0bb82a Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 4 May 2023 16:18:29 -0600 Subject: [PATCH 39/80] Update the readme.md to clarify that it is disabled and that it only supports bazel_dep. --- lib/modules/manager/bazel-module/readme.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md index 3e15418c474e13..5c7fe04b9bcad9 100644 --- a/lib/modules/manager/bazel-module/readme.md +++ b/lib/modules/manager/bazel-module/readme.md @@ -1,6 +1,8 @@ !!! warning - The `bazel-module` manager is work-in-progress. + The `bazel-module` manager is a work-in-progress. + It is currently disabled. + The manager only supports processing of `bazel_dep` declarations. For more information, see [issue 13658](https://github.com/renovatebot/renovate/issues/13658). The `bazel-module` manager supports the maintenance of [Bazel module (bzlmod)](https://bazel.build/external/module) enabled workspaces. From cd5a628aaf8de8e1c4a66f6b25e48fe119f05bb3 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 5 May 2023 15:43:02 -0600 Subject: [PATCH 40/80] Rename filters --- lib/modules/manager/bazel-module/extract.ts | 4 ++-- lib/modules/manager/bazel-module/filters.spec.ts | 6 +++--- lib/modules/manager/bazel-module/filters.ts | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index f7173fda75ab96..e7aa987c59164f 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,6 +1,6 @@ import type { PackageFileContent } from '../types'; import { BazelDepRecordToPackageDependency } from './bazel-dep'; -import { instanceExists } from './filters'; +import { exists } from './filters'; import { parse } from './parser'; export function extractPackageFile( @@ -17,7 +17,7 @@ export function extractPackageFile( // istanbul ignore next: cannot reach undefined without an additional rule return result.success ? result.data : undefined; }) - .filter(instanceExists); + .filter(exists); // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/filters.spec.ts b/lib/modules/manager/bazel-module/filters.spec.ts index baf7bf3803d71e..8b1ad2ee94bd8e 100644 --- a/lib/modules/manager/bazel-module/filters.spec.ts +++ b/lib/modules/manager/bazel-module/filters.spec.ts @@ -1,13 +1,13 @@ -import { instanceExists } from './filters'; +import { exists } from './filters'; describe('modules/manager/bazel-module/filters', () => { - it('instanceExists', () => { + it('exists', () => { const array: Array = [ 'first', null, 'second', undefined, ]; - expect(array.filter(instanceExists)).toEqual(['first', 'second']); + expect(array.filter(exists)).toEqual(['first', 'second']); }); }); diff --git a/lib/modules/manager/bazel-module/filters.ts b/lib/modules/manager/bazel-module/filters.ts index 30d3ff5746a7f6..fa9cd12146621e 100644 --- a/lib/modules/manager/bazel-module/filters.ts +++ b/lib/modules/manager/bazel-module/filters.ts @@ -2,9 +2,7 @@ import is from '@sindresorhus/is'; // Filter out missing values in a typesafe manner. // Inspired by https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array -export function instanceExists( - value: Value | null | undefined -): value is Value { +export function exists(value: Value | null | undefined): value is Value { if (is.falsy(value)) { return false; } From 2630b715af1962fb1bf891b1f9218b5f261a9381 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 5 May 2023 16:02:26 -0600 Subject: [PATCH 41/80] Remove StarlarkBoolean --- lib/modules/manager/bazel-module/fragments.ts | 5 ++-- lib/modules/manager/bazel-module/parser.ts | 8 +++--- .../manager/bazel-module/starlark.spec.ts | 16 +++++------ lib/modules/manager/bazel-module/starlark.ts | 27 +++++++------------ 4 files changed, 23 insertions(+), 33 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 5eb51d1ea8a78a..8829bf8516e099 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -1,6 +1,6 @@ import is from '@sindresorhus/is'; import { z } from 'zod'; -import { StarlarkBoolean } from './starlark'; +import * as starlark from './starlark'; // Fragment Schemas @@ -93,8 +93,7 @@ export class BooleanFragment implements FragmentCompatible { readonly isComplete = true; readonly value: boolean; constructor(input: boolean | string) { - this.value = - typeof input === 'string' ? StarlarkBoolean.asBoolean(input) : input; + this.value = typeof input === 'string' ? starlark.asBoolean(input) : input; } } diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index f24d6086211a26..7ae983dbea7f8d 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -3,9 +3,9 @@ import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { Ctx } from './context'; import type { ValueFragment } from './fragments'; -import { StarlarkBoolean } from './starlark'; +import * as starlark from './starlark'; -const booleanValuesRegex = regEx(`^${StarlarkBoolean.stringValues.join('|')}$`); +const booleanValuesRegex = regEx(`^${starlark.booleanStringValues.join('|')}$`); const supportedRules = ['bazel_dep']; const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); @@ -51,7 +51,7 @@ const query = q.tree({ search: rule, }); -const starlark = lang.createLang('starlark'); +const starlarkLang = lang.createLang('starlark'); export function parse( input: string, @@ -59,7 +59,7 @@ export function parse( ): ValueFragment[] | null { let result: ValueFragment[] | null = null; try { - const parsedResult = starlark.query(input, query, new Ctx()); + const parsedResult = starlarkLang.query(input, query, new Ctx()); if (parsedResult) { // The parsedResult and its associated objects are missing their types. result = Ctx.as(parsedResult).results; diff --git a/lib/modules/manager/bazel-module/starlark.spec.ts b/lib/modules/manager/bazel-module/starlark.spec.ts index f015d179f5c2bd..51d8b5cd18e68b 100644 --- a/lib/modules/manager/bazel-module/starlark.spec.ts +++ b/lib/modules/manager/bazel-module/starlark.spec.ts @@ -1,13 +1,11 @@ -import { StarlarkBoolean } from './starlark'; +import * as starlark from './starlark'; describe('modules/manager/bazel-module/starlark', () => { - describe('StarlarkBoolean', () => { - it('asBoolean', () => { - expect(StarlarkBoolean.asBoolean('True')).toBe(true); - expect(StarlarkBoolean.asBoolean('False')).toBe(false); - expect(() => StarlarkBoolean.asBoolean('bad')).toThrow( - new Error('Invalid Starlark boolean string: bad') - ); - }); + it('asBoolean', () => { + expect(starlark.asBoolean('True')).toBe(true); + expect(starlark.asBoolean('False')).toBe(false); + expect(() => starlark.asBoolean('bad')).toThrow( + new Error('Invalid Starlark boolean string: bad') + ); }); }); diff --git a/lib/modules/manager/bazel-module/starlark.ts b/lib/modules/manager/bazel-module/starlark.ts index 35f922bae28bd3..9165fe5c3ef712 100644 --- a/lib/modules/manager/bazel-module/starlark.ts +++ b/lib/modules/manager/bazel-module/starlark.ts @@ -1,21 +1,14 @@ -export class StarlarkBoolean { - static readonly stringMapping: ReadonlyMap = new Map< - string, - boolean - >([ - ['True', true], - ['False', false], - ]); +const stringMapping: ReadonlyMap = new Map([ + ['True', true], + ['False', false], +]); - static readonly stringValues = Array.from( - StarlarkBoolean.stringMapping.keys() - ); +export const booleanStringValues = Array.from(stringMapping.keys()); - static asBoolean(value: string): boolean { - const result = StarlarkBoolean.stringMapping.get(value); - if (result !== undefined) { - return result; - } - throw new Error(`Invalid Starlark boolean string: ${value}`); +export function asBoolean(value: string): boolean { + const result = stringMapping.get(value); + if (result !== undefined) { + return result; } + throw new Error(`Invalid Starlark boolean string: ${value}`); } From a777e537e8bce667a511072069bc071db56dc3d0 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 05:37:02 -0600 Subject: [PATCH 42/80] Migrate test to it.each --- lib/modules/manager/bazel-module/starlark.spec.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/starlark.spec.ts b/lib/modules/manager/bazel-module/starlark.spec.ts index 51d8b5cd18e68b..efd3e50ab19a74 100644 --- a/lib/modules/manager/bazel-module/starlark.spec.ts +++ b/lib/modules/manager/bazel-module/starlark.spec.ts @@ -1,9 +1,15 @@ import * as starlark from './starlark'; describe('modules/manager/bazel-module/starlark', () => { + it.each` + a | exp + ${'True'} | ${true} + ${'False'} | ${false} + `('.asBoolean($a)', ({ a, exp }) => { + expect(starlark.asBoolean(a)).toBe(exp); + }); + it('asBoolean', () => { - expect(starlark.asBoolean('True')).toBe(true); - expect(starlark.asBoolean('False')).toBe(false); expect(() => starlark.asBoolean('bad')).toThrow( new Error('Invalid Starlark boolean string: bad') ); From 958d30f33851134c83195662654128fa6b9cb42c Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 05:42:36 -0600 Subject: [PATCH 43/80] Add test cases to extract.spec.ts --- lib/modules/manager/bazel-module/extract.spec.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 2fc65da42ef2d8..6182e57424b481 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -9,6 +9,19 @@ describe('modules/manager/bazel-module/extract', () => { expect(result).toBeNull(); }); + it('returns null if file is empty', () => { + const result = extractPackageFile('', 'MODULE.bazel'); + expect(result).toBeNull(); + }); + + it('returns null if file has not recognized declarations', () => { + const input = codeBlock` + ignore_me(name = "rules_foo", version = "1.2.3") + `; + const result = extractPackageFile(input, 'MODULE.bazel'); + expect(result).toBeNull(); + }); + it('returns dependencies', () => { const input = codeBlock` bazel_dep(name = "rules_foo", version = "1.2.3") From 803748272d9bcc75e4f4312c54c3430b77b1d9e1 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 05:56:28 -0600 Subject: [PATCH 44/80] Use is.plainObject instead of exists --- lib/modules/manager/bazel-module/extract.ts | 4 ++-- lib/modules/manager/bazel-module/filters.spec.ts | 13 ------------- lib/modules/manager/bazel-module/filters.ts | 11 ----------- 3 files changed, 2 insertions(+), 26 deletions(-) delete mode 100644 lib/modules/manager/bazel-module/filters.spec.ts delete mode 100644 lib/modules/manager/bazel-module/filters.ts diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index e7aa987c59164f..afb20c88ee8b64 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,6 +1,6 @@ +import is from '@sindresorhus/is'; import type { PackageFileContent } from '../types'; import { BazelDepRecordToPackageDependency } from './bazel-dep'; -import { exists } from './filters'; import { parse } from './parser'; export function extractPackageFile( @@ -17,7 +17,7 @@ export function extractPackageFile( // istanbul ignore next: cannot reach undefined without an additional rule return result.success ? result.data : undefined; }) - .filter(exists); + .filter(is.plainObject); // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/filters.spec.ts b/lib/modules/manager/bazel-module/filters.spec.ts deleted file mode 100644 index 8b1ad2ee94bd8e..00000000000000 --- a/lib/modules/manager/bazel-module/filters.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { exists } from './filters'; - -describe('modules/manager/bazel-module/filters', () => { - it('exists', () => { - const array: Array = [ - 'first', - null, - 'second', - undefined, - ]; - expect(array.filter(exists)).toEqual(['first', 'second']); - }); -}); diff --git a/lib/modules/manager/bazel-module/filters.ts b/lib/modules/manager/bazel-module/filters.ts deleted file mode 100644 index fa9cd12146621e..00000000000000 --- a/lib/modules/manager/bazel-module/filters.ts +++ /dev/null @@ -1,11 +0,0 @@ -import is from '@sindresorhus/is'; - -// Filter out missing values in a typesafe manner. -// Inspired by https://stackoverflow.com/questions/43118692/typescript-filter-out-nulls-from-an-array -export function exists(value: Value | null | undefined): value is Value { - if (is.falsy(value)) { - return false; - } - const placeholder: Value = value; - return is.truthy(placeholder); -} From eb77be123c8575eb2722d6a3412a678488df800e Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 06:23:58 -0600 Subject: [PATCH 45/80] Switch to is.boolean --- lib/modules/manager/bazel-module/starlark.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/starlark.ts b/lib/modules/manager/bazel-module/starlark.ts index 9165fe5c3ef712..7fb06d2db338bb 100644 --- a/lib/modules/manager/bazel-module/starlark.ts +++ b/lib/modules/manager/bazel-module/starlark.ts @@ -1,3 +1,5 @@ +import is from '@sindresorhus/is'; + const stringMapping: ReadonlyMap = new Map([ ['True', true], ['False', false], @@ -7,7 +9,7 @@ export const booleanStringValues = Array.from(stringMapping.keys()); export function asBoolean(value: string): boolean { const result = stringMapping.get(value); - if (result !== undefined) { + if (is.boolean(result)) { return result; } throw new Error(`Invalid Starlark boolean string: ${value}`); From 9907ff096b929ed6ac04ec299f1367441c04fc60 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 11:54:43 -0600 Subject: [PATCH 46/80] Add mock test for extractPackageFile --- .../manager/bazel-module/extract-mock.spec.ts | 17 +++++++++++++++++ lib/modules/manager/bazel-module/extract.ts | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 lib/modules/manager/bazel-module/extract-mock.spec.ts diff --git a/lib/modules/manager/bazel-module/extract-mock.spec.ts b/lib/modules/manager/bazel-module/extract-mock.spec.ts new file mode 100644 index 00000000000000..7e19ad2549e992 --- /dev/null +++ b/lib/modules/manager/bazel-module/extract-mock.spec.ts @@ -0,0 +1,17 @@ +import { mocked } from '../../../../test/util'; +import { extractPackageFile } from './extract'; +import { RecordFragment, ValueFragment } from './fragments'; +import * as _parser from './parser'; + +jest.mock('./parser'); +const parser = mocked(_parser); + +describe('modules/manager/bazel-module/extract-mock', () => { + describe('extractPackageFile', () => { + it('gracefully handles unexpected fragments', () => { + const fragments: ValueFragment[] = [new RecordFragment()]; + parser.parse.mockReturnValueOnce(fragments); + expect(extractPackageFile('', 'MODULE.bazel')).toBeNull(); + }); + }); +}); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index afb20c88ee8b64..4b65f295b2acff 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -14,7 +14,6 @@ export function extractPackageFile( const deps = fragments .map((record) => { const result = BazelDepRecordToPackageDependency.safeParse(record); - // istanbul ignore next: cannot reach undefined without an additional rule return result.success ? result.data : undefined; }) .filter(is.plainObject); From d5ee2a5d47ac70c20253244e5f73b2b3e16916af Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 11:58:38 -0600 Subject: [PATCH 47/80] Return null instead of undefined --- lib/modules/manager/bazel-module/fragments.spec.ts | 2 +- lib/modules/manager/bazel-module/fragments.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index df492505a52da5..ea2a6ac155af7b 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -135,7 +135,7 @@ describe('modules/manager/bazel-module/fragments', () => { ${new StringFragment('hello')} | ${new StringFragment('hello')} ${new ArrayFragment()} | ${new ArrayFragment()} ${new RecordFragment()} | ${new RecordFragment()} - ${new AttributeFragment('name')} | ${undefined} + ${new AttributeFragment('name')} | ${null} `('Fragments.safeAsValue($frag)', ({ frag, exp }) => { const result = Fragments.safeAsValue(frag); expect(result).toEqual(exp); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 8829bf8516e099..7c515c156ba37b 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -130,13 +130,13 @@ export class ArrayFragment implements FragmentCompatible { export class RecordFragment implements FragmentCompatible { static readonly schema = RecordFragmentSchema; - static safeAs(frag: FragmentCompatible): RecordFragment | undefined { + static safeAs(frag: FragmentCompatible): RecordFragment | null { if (frag instanceof RecordFragment) { return frag; } const parseResult = RecordFragmentSchema.safeParse(frag); if (!parseResult.success) { - return undefined; + return null; } Object.setPrototypeOf(frag, RecordFragment.prototype); const record = frag as RecordFragment; @@ -175,13 +175,13 @@ export class RecordFragment implements FragmentCompatible { export class AttributeFragment implements FragmentCompatible { static readonly schema = AttributeFragmentSchema; - static safeAs(frag: FragmentCompatible): AttributeFragment | undefined { + static safeAs(frag: FragmentCompatible): AttributeFragment | null { if (frag instanceof AttributeFragment) { return frag; } const parseResult = AttributeFragmentSchema.safeParse(frag); if (!parseResult.success) { - return undefined; + return null; } Object.setPrototypeOf(frag, AttributeFragment.prototype); const attribute = frag as AttributeFragment; @@ -232,7 +232,7 @@ export class Fragments { return new Error(`Expected type ${expected}, but was ${actual}.`); } - static safeAsValue(frag: FragmentCompatible): ValueFragment | undefined { + static safeAsValue(frag: FragmentCompatible): ValueFragment | null { switch (frag.type) { case 'string': return StringFragment.as(frag); @@ -243,7 +243,7 @@ export class Fragments { case 'record': return RecordFragment.as(frag); default: - return undefined; + return null; } } From 4fda799eb8634db78c8652ca6de416ab604b86cc Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 12:04:39 -0600 Subject: [PATCH 48/80] Remove extraneous Ctx.as() --- lib/modules/manager/bazel-module/parser.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 7ae983dbea7f8d..6e9f6f5e72e26f 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -61,8 +61,7 @@ export function parse( try { const parsedResult = starlarkLang.query(input, query, new Ctx()); if (parsedResult) { - // The parsedResult and its associated objects are missing their types. - result = Ctx.as(parsedResult).results; + result = parsedResult.results; } } catch (err) /* istanbul ignore next */ { logger.debug({ err, packageFile }, 'Bazel module parsing error'); From 8b5aaaec5011463606c558ffb5024e7d1a39ecd8 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 8 May 2023 12:39:03 -0600 Subject: [PATCH 49/80] Switch extract mock test to use spyOn --- .../manager/bazel-module/extract-mock.spec.ts | 17 ----------------- .../manager/bazel-module/extract.spec.ts | 9 +++++++++ 2 files changed, 9 insertions(+), 17 deletions(-) delete mode 100644 lib/modules/manager/bazel-module/extract-mock.spec.ts diff --git a/lib/modules/manager/bazel-module/extract-mock.spec.ts b/lib/modules/manager/bazel-module/extract-mock.spec.ts deleted file mode 100644 index 7e19ad2549e992..00000000000000 --- a/lib/modules/manager/bazel-module/extract-mock.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { mocked } from '../../../../test/util'; -import { extractPackageFile } from './extract'; -import { RecordFragment, ValueFragment } from './fragments'; -import * as _parser from './parser'; - -jest.mock('./parser'); -const parser = mocked(_parser); - -describe('modules/manager/bazel-module/extract-mock', () => { - describe('extractPackageFile', () => { - it('gracefully handles unexpected fragments', () => { - const fragments: ValueFragment[] = [new RecordFragment()]; - parser.parse.mockReturnValueOnce(fragments); - expect(extractPackageFile('', 'MODULE.bazel')).toBeNull(); - }); - }); -}); diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 6182e57424b481..de53bc9925f437 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -1,5 +1,7 @@ import { codeBlock } from 'common-tags'; import { BazelDatasource } from '../../datasource/bazel'; +import { RecordFragment, ValueFragment } from './fragments'; +import * as parser from './parser'; import { extractPackageFile } from '.'; describe('modules/manager/bazel-module/extract', () => { @@ -22,6 +24,13 @@ describe('modules/manager/bazel-module/extract', () => { expect(result).toBeNull(); }); + it('gracefully handles unexpected fragments', () => { + const fragments: ValueFragment[] = [new RecordFragment()]; + const mockParse = jest.spyOn(parser, 'parse'); + mockParse.mockReturnValueOnce(fragments); + expect(extractPackageFile('', 'MODULE.bazel')).toBeNull(); + }); + it('returns dependencies', () => { const input = codeBlock` bazel_dep(name = "rules_foo", version = "1.2.3") From a499e873870047225156d1ec5356a0fd3f05c71a Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 9 May 2023 08:58:30 -0600 Subject: [PATCH 50/80] Change wording for readme.md. --- lib/modules/manager/bazel-module/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/readme.md b/lib/modules/manager/bazel-module/readme.md index 5c7fe04b9bcad9..06ce659dcaf89f 100644 --- a/lib/modules/manager/bazel-module/readme.md +++ b/lib/modules/manager/bazel-module/readme.md @@ -2,7 +2,7 @@ !!! warning The `bazel-module` manager is a work-in-progress. It is currently disabled. - The manager only supports processing of `bazel_dep` declarations. + The manager only supports updating `bazel_dep` declarations. For more information, see [issue 13658](https://github.com/renovatebot/renovate/issues/13658). -The `bazel-module` manager supports the maintenance of [Bazel module (bzlmod)](https://bazel.build/external/module) enabled workspaces. +The `bazel-module` manager can update [Bazel module (bzlmod)](https://bazel.build/external/module) enabled workspaces. From c152124d166a44072f729e5abe65ed950b7d8b25 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 9 May 2023 11:57:37 -0600 Subject: [PATCH 51/80] Rework fragments to use discriminatedUnion --- .../manager/bazel-module/bazel-dep.spec.ts | 4 +- lib/modules/manager/bazel-module/bazel-dep.ts | 37 +-- lib/modules/manager/bazel-module/context.ts | 7 +- lib/modules/manager/bazel-module/extract.ts | 7 +- .../manager/bazel-module/fragments.spec.ts | 27 +- lib/modules/manager/bazel-module/fragments.ts | 313 +++++++++--------- 6 files changed, 196 insertions(+), 199 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts index 44d180d9b76795..8732dc88bd2604 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -1,5 +1,5 @@ import { BazelDatasource } from '../../datasource/bazel'; -import { BazelDepRecordToPackageDependency } from './bazel-dep'; +import { toPackageDependency } from './bazel-dep'; import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; describe('modules/manager/bazel-module/bazel-dep', () => { @@ -11,7 +11,7 @@ describe('modules/manager/bazel-module/bazel-dep', () => { version: new StringFragment('1.2.3'), dev_dependency: new BooleanFragment(true), }); - const result = BazelDepRecordToPackageDependency.parse(record); + const result = toPackageDependency(record); expect(result).toEqual({ datasource: BazelDatasource.id, depType: 'bazel_dep', diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index 81774e83624a5a..23b9d2f3630080 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -1,28 +1,23 @@ -import { z } from 'zod'; +import is from '@sindresorhus/is'; import { BazelDatasource } from '../../datasource/bazel'; import type { PackageDependency } from '../types'; -import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; +import { RecordFragment, StringFragment, ValueFragment } from './fragments'; -const BazelDepRecord = RecordFragment.schema - .extend({ - children: z.object({ - rule: StringFragment.schema.extend({ - value: z.enum(['bazel_dep']), - }), - name: StringFragment.schema, - version: StringFragment.schema, - dev_dependency: BooleanFragment.schema.optional(), - }), - }) - .transform((frag): RecordFragment => { - return RecordFragment.as(frag); - }); - -export const BazelDepRecordToPackageDependency = BazelDepRecord.transform( - ({ children: { rule, name, version } }): PackageDependency => ({ +export function toPackageDependency( + value: ValueFragment +): PackageDependency | null { + const record = RecordFragment.safeAs(value); + if (!record) { + return null; + } + const { rule, name, version } = record.children; + if (is.falsy(rule) || is.falsy(name) || is.falsy(version)) { + return null; + } + return { datasource: BazelDatasource.id, depType: StringFragment.as(rule).value, depName: StringFragment.as(name).value, currentValue: StringFragment.as(version).value, - }) -); + }; +} diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 1f1f1947863a81..79fac7ce1b99cd 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -4,10 +4,11 @@ import { BooleanFragment, ChildFragments, Fragment, - Fragments, RecordFragment, StringFragment, ValueFragment, + asFragment, + safeAsValue, } from './fragments'; import { Stack } from './stack'; @@ -27,7 +28,7 @@ export class Ctx implements CtxCompatible { static as(obj: CtxCompatible): Ctx { Object.setPrototypeOf(obj, Ctx.prototype); const ctx = obj as Ctx; - const stackItems = ctx.stack.map((item) => Fragments.asFragment(item)); + const stackItems = ctx.stack.map((item) => asFragment(item)); ctx.stack = Stack.create(...stackItems); ctx.results = ctx.results.map((item) => RecordFragment.as(item)); return ctx; @@ -56,7 +57,7 @@ export class Ctx implements CtxCompatible { return; } const parent = this.stack.safeCurrent; - const value = Fragments.safeAsValue(current); + const value = safeAsValue(current); if (value) { if (parent && 'addValue' in parent) { parent.addValue(value); diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 4b65f295b2acff..dd8e075b5f29d5 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,6 +1,6 @@ import is from '@sindresorhus/is'; import type { PackageFileContent } from '../types'; -import { BazelDepRecordToPackageDependency } from './bazel-dep'; +import * as bazelDep from './bazel-dep'; import { parse } from './parser'; export function extractPackageFile( @@ -12,10 +12,7 @@ export function extractPackageFile( return null; } const deps = fragments - .map((record) => { - const result = BazelDepRecordToPackageDependency.safeParse(record); - return result.success ? result.data : undefined; - }) + .map((frag) => bazelDep.toPackageDependency(frag)) .filter(is.plainObject); // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index ea2a6ac155af7b..a052a545d1ff69 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -3,10 +3,12 @@ import { AttributeFragment, BooleanFragment, FragmentType, - Fragments, RecordFragment, StringFragment, ValueFragment, + asFragment, + asValue, + safeAsValue, } from './fragments'; describe('modules/manager/bazel-module/fragments', () => { @@ -113,18 +115,13 @@ describe('modules/manager/bazel-module/fragments', () => { describe('as', () => { it('returns an AttributeFragment', () => { - const frag: { - type: FragmentType; - isComplete: boolean; - value?: ValueFragment; - } = { + const frag = { type: 'attribute', - isComplete: false, + name: 'greeting', value: new StringFragment('hello'), }; const result = AttributeFragment.as(frag); expect(result).toEqual(frag); - expect(result).toBeInstanceOf(AttributeFragment); }); }); }); @@ -136,23 +133,21 @@ describe('modules/manager/bazel-module/fragments', () => { ${new ArrayFragment()} | ${new ArrayFragment()} ${new RecordFragment()} | ${new RecordFragment()} ${new AttributeFragment('name')} | ${null} - `('Fragments.safeAsValue($frag)', ({ frag, exp }) => { - const result = Fragments.safeAsValue(frag); + `('safeAsValue($frag)', ({ frag, exp }) => { + const result = safeAsValue(frag); expect(result).toEqual(exp); }); describe('asValue', () => { it('returns the instance when it is a value fragment', () => { const value = new StringFragment('hello'); - const result = Fragments.asValue(value); + const result = asValue(value); expect(result).toEqual(value); }); it('throws when it is not a value fragment', () => { const attribute = new AttributeFragment('name'); - expect(() => Fragments.asValue(attribute)).toThrow( - new Error(`Unexpected fragment type: attribute`) - ); + expect(() => asValue(attribute)).toThrow(); }); }); @@ -162,8 +157,8 @@ describe('modules/manager/bazel-module/fragments', () => { ${new ArrayFragment()} | ${new ArrayFragment()} ${new RecordFragment()} | ${new RecordFragment()} ${new AttributeFragment('name')} | ${new AttributeFragment('name')} - `('Fragments.asFragment($frag)', ({ frag, exp }) => { - const result = Fragments.asFragment(frag); + `('asFragment($frag)', ({ frag, exp }) => { + const result = asFragment(frag); expect(result).toEqual(exp); }); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 7c515c156ba37b..136cf28763fc1f 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -4,47 +4,58 @@ import * as starlark from './starlark'; // Fragment Schemas -const FragmentTypeSchema = z.enum([ - 'string', - 'boolean', - 'array', - 'record', - 'attribute', -]); -const CommonFragmentSchema = z.object({ - type: FragmentTypeSchema, -}); -const CompletableSchema = z.object({ - isComplete: z.boolean(), -}); -const ChildFragmentsSchema = z.record(z.string(), CommonFragmentSchema); -const StringFragmentSchema = CommonFragmentSchema.merge( - CompletableSchema -).extend({ - type: z.enum(['string']), - value: z.string(), -}); -const BooleanFragmentSchema = CommonFragmentSchema.merge( - CompletableSchema -).extend({ - type: z.enum(['boolean']), - value: z.boolean(), -}); -const ArrayFragmentSchema = CommonFragmentSchema.merge( - CompletableSchema -).extend({ - type: z.enum(['array']), - items: z.array(CommonFragmentSchema), -}); -const RecordFragmentSchema = CommonFragmentSchema.merge( - CompletableSchema -).extend({ - type: z.enum(['record']), - children: ChildFragmentsSchema, -}); -const AttributeFragmentSchema = CommonFragmentSchema.extend({ - type: z.enum(['attribute']), - value: CommonFragmentSchema.optional(), +interface StringFragmentInterface { + readonly type: 'string'; + readonly value: string; +} + +interface BooleanFragmentInterface { + readonly type: 'boolean'; + readonly value: boolean; +} + +interface ArrayFragmentInterface { + readonly type: 'array'; + items: ValueFragmentInterface[]; + isComplete: boolean; +} + +type ChildFragmentsInterface = Record; + +interface RecordFragmentInterface { + readonly type: 'record'; + children: ChildFragmentsInterface; + isComplete: boolean; +} + +type ValueFragmentInterface = + | StringFragmentInterface + | BooleanFragmentInterface + | ArrayFragmentInterface + | RecordFragmentInterface; + +export const ValueFragmentSchema: z.ZodType = z.lazy( + () => + z.discriminatedUnion('type', [ + z.object({ type: z.literal('string'), value: z.string() }), + z.object({ type: z.literal('boolean'), value: z.boolean() }), + z.object({ + type: z.literal('array'), + items: ValueFragmentSchema.array(), + isComplete: z.boolean(), + }), + z.object({ + type: z.literal('record'), + children: z.record(z.string(), ValueFragmentSchema), + isComplete: z.boolean(), + }), + ]) +); + +const AttributeFragmentSchema = z.object({ + type: z.literal('attribute'), + name: z.string(), + value: ValueFragmentSchema.optional(), }); // Fragment Types @@ -56,20 +67,20 @@ export type FragmentType = | 'record' | 'attribute'; -export interface FragmentCompatible { - readonly type: FragmentType; -} - -export class StringFragment implements FragmentCompatible { - static readonly schema = StringFragmentSchema; - - static as(frag: FragmentCompatible): StringFragment { - if (frag instanceof StringFragment) { - return frag; +export class StringFragment { + static as(data: unknown): StringFragment { + if (data instanceof StringFragment) { + return data; } - StringFragmentSchema.parse(frag); - Object.setPrototypeOf(frag, StringFragment.prototype); - return frag as StringFragment; + const frag = ValueFragmentSchema.parse(data); + if (frag.type === 'string') { + return StringFragment.from(frag); + } + throw typeError('string', frag.type); + } + + static from(frag: StringFragmentInterface): StringFragment { + return new StringFragment(frag.value); } readonly type: FragmentType = 'string'; @@ -77,16 +88,20 @@ export class StringFragment implements FragmentCompatible { constructor(readonly value: string) {} } -export class BooleanFragment implements FragmentCompatible { - static readonly schema = BooleanFragmentSchema; - - static as(frag: FragmentCompatible): BooleanFragment { - if (frag instanceof BooleanFragment) { - return frag; +export class BooleanFragment { + static as(data: unknown): BooleanFragment { + if (data instanceof BooleanFragment) { + return data; + } + const frag = ValueFragmentSchema.parse(data); + if (frag.type === 'boolean') { + return BooleanFragment.from(frag); } - BooleanFragmentSchema.parse(frag); - Object.setPrototypeOf(frag, BooleanFragment.prototype); - return frag as BooleanFragment; + throw typeError('boolean', frag.type); + } + + static from(frag: BooleanFragmentInterface): BooleanFragment { + return new BooleanFragment(frag.value); } readonly type: FragmentType = 'boolean'; @@ -97,20 +112,23 @@ export class BooleanFragment implements FragmentCompatible { } } -export class ArrayFragment implements FragmentCompatible { - static readonly schema = ArrayFragmentSchema; - - static as(frag: FragmentCompatible): ArrayFragment { - if (frag instanceof ArrayFragment) { - return frag; +export class ArrayFragment { + static as(data: unknown): ArrayFragment { + if (data instanceof ArrayFragment) { + return data; } - ArrayFragmentSchema.parse(frag); - Object.setPrototypeOf(frag, ArrayFragment.prototype); - const array = frag as ArrayFragment; - for (let i = 0; i < array.items.length; i++) { - array.items[i] = Fragments.asValue(array.items[i]); + const frag = ValueFragmentSchema.parse(data); + if (frag.type === 'array') { + return ArrayFragment.from(frag); } - return array; + throw typeError('array', frag.type); + } + + static from(frag: ArrayFragmentInterface): ArrayFragment { + return new ArrayFragment( + frag.items.map((item) => asValue(item)), + frag.isComplete + ); } readonly type: FragmentType = 'array'; @@ -127,36 +145,39 @@ export class ArrayFragment implements FragmentCompatible { } } -export class RecordFragment implements FragmentCompatible { - static readonly schema = RecordFragmentSchema; - - static safeAs(frag: FragmentCompatible): RecordFragment | null { - if (frag instanceof RecordFragment) { - return frag; - } - const parseResult = RecordFragmentSchema.safeParse(frag); - if (!parseResult.success) { - return null; +export class RecordFragment { + static as(data: unknown): RecordFragment { + if (data instanceof RecordFragment) { + return data; } - Object.setPrototypeOf(frag, RecordFragment.prototype); - const record = frag as RecordFragment; - for (const prop in record.children) { - const child = record.children[prop]; - record.children[prop] = Fragments.asValue(child); + const frag = ValueFragmentSchema.parse(data); + if (frag.type !== 'record') { + throw typeError('record', frag.type); } - return record; + return RecordFragment.from(frag); + } + + static from(frag: RecordFragmentInterface): RecordFragment { + const children: ChildFragments = Object.entries(frag.children).reduce( + (acc, propVal) => { + acc[propVal[0]] = asValue(propVal[1]); + return acc; + }, + {} as ChildFragments + ); + return new RecordFragment(children, frag.isComplete); } - static as(frag: FragmentCompatible): RecordFragment { - const record = RecordFragment.safeAs(frag); - if (record) { - return record; + static safeAs(data: unknown): RecordFragment | null { + try { + return RecordFragment.as(data); + } catch (e) { + return null; } - throw Fragments.typeError('record', frag.type); } readonly type: FragmentType = 'record'; - isComplete = false; + isComplete: boolean; children: ChildFragments; constructor(children: ChildFragments = {}, isComplete = false) { @@ -172,31 +193,24 @@ export class RecordFragment implements FragmentCompatible { } } -export class AttributeFragment implements FragmentCompatible { - static readonly schema = AttributeFragmentSchema; - - static safeAs(frag: FragmentCompatible): AttributeFragment | null { - if (frag instanceof AttributeFragment) { - return frag; +export class AttributeFragment { + static as(data: unknown): AttributeFragment { + if (data instanceof AttributeFragment) { + return data; } - const parseResult = AttributeFragmentSchema.safeParse(frag); - if (!parseResult.success) { - return null; - } - Object.setPrototypeOf(frag, AttributeFragment.prototype); - const attribute = frag as AttributeFragment; - if (attribute.value) { - attribute.value = Fragments.asValue(attribute.value); - } - return attribute; + const frag = AttributeFragmentSchema.parse(data); + return new AttributeFragment( + frag.name, + frag.value ? asValue(frag.value) : undefined + ); } - static as(frag: FragmentCompatible): AttributeFragment { - const attribute = AttributeFragment.safeAs(frag); - if (attribute) { - return attribute; + static safeAs(data: unknown): AttributeFragment | null { + try { + return AttributeFragment.as(data); + } catch (e) { + return null; } - throw Fragments.typeError('attribute', frag.type); } readonly type: FragmentType = 'attribute'; @@ -225,45 +239,40 @@ export type ValueFragment = export type ChildFragments = Record; export type Fragment = ValueFragment | AttributeFragment; -// Fragments Class +// Static Functions -export class Fragments { - static typeError(expected: string, actual: string): Error { - return new Error(`Expected type ${expected}, but was ${actual}.`); - } +function typeError(expected: string, actual: string): Error { + return new Error(`Expected type ${expected}, but was ${actual}.`); +} - static safeAsValue(frag: FragmentCompatible): ValueFragment | null { - switch (frag.type) { - case 'string': - return StringFragment.as(frag); - case 'boolean': - return BooleanFragment.as(frag); - case 'array': - return ArrayFragment.as(frag); - case 'record': - return RecordFragment.as(frag); - default: - return null; - } +export function asValue(data: unknown): ValueFragment { + const frag = ValueFragmentSchema.parse(data); + switch (frag.type) { + case 'string': + return StringFragment.from(frag); + case 'boolean': + return BooleanFragment.from(frag); + case 'array': + return ArrayFragment.from(frag); + case 'record': + return RecordFragment.from(frag); + default: + throw new Error('Unexpected fragment type.'); } +} - static asValue(frag: FragmentCompatible): ValueFragment { - const value = Fragments.safeAsValue(frag); - if (value) { - return value; - } - throw new Error(`Unexpected fragment type: ${frag.type}`); +export function safeAsValue(data: unknown): ValueFragment | null { + try { + return asValue(data); + } catch (e) { + return null; } +} - static asFragment(frag: FragmentCompatible): Fragment { - const value = Fragments.safeAsValue(frag); - if (value) { - return value; - } - if (frag.type === 'attribute') { - return AttributeFragment.as(frag); - } - // istanbul ignore next: can only get here if new type addded, but no impl - throw new Error(`Unexpected fragment type: ${frag.type}`); +export function asFragment(data: unknown): Fragment { + const value = safeAsValue(data); + if (value) { + return value; } + return AttributeFragment.as(data); } From a2c879f9c42496565dced35baa7116b6fa47711f Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 9 May 2023 16:52:46 -0600 Subject: [PATCH 52/80] -m --- .../manager/bazel-module/fragments.spec.ts | 18 +++-- lib/modules/manager/bazel-module/fragments.ts | 76 ++++++++++--------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index a052a545d1ff69..ced0750a94c3c7 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -2,10 +2,8 @@ import { ArrayFragment, AttributeFragment, BooleanFragment, - FragmentType, RecordFragment, StringFragment, - ValueFragment, asFragment, asValue, safeAsValue, @@ -61,11 +59,7 @@ describe('modules/manager/bazel-module/fragments', () => { describe('as', () => { it('returns an ArrayFragment', () => { - const frag: { - type: FragmentType; - isComplete: boolean; - items: ValueFragment[]; - } = { + const frag = { type: 'array', isComplete: false, items: [new StringFragment('hello')], @@ -171,5 +165,15 @@ describe('modules/manager/bazel-module/fragments', () => { `('$fn throws with $frag', ({ fn, frag }) => { expect(() => fn(frag)).toThrow(); }); + + it.each` + fn | data | exp + ${StringFragment.as} | ${{ type: 'string', value: 'hello' }} | ${new StringFragment('hello')} + ${BooleanFragment.as} | ${{ type: 'boolean', value: true }} | ${new BooleanFragment(true)} + ${ArrayFragment.as} | ${{ type: 'array', isComplete: false, items: [] }} | ${new ArrayFragment()} + ${RecordFragment.as} | ${{ type: 'record', isComplete: false, children: {} }} | ${new RecordFragment()} + `('$fn with $data', ({ fn, data, exp }) => { + expect(fn(data)).toEqual(exp); + }); }); }); diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 136cf28763fc1f..83d186739ad97f 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -69,14 +69,13 @@ export type FragmentType = export class StringFragment { static as(data: unknown): StringFragment { - if (data instanceof StringFragment) { - return data; - } - const frag = ValueFragmentSchema.parse(data); - if (frag.type === 'string') { - return StringFragment.from(frag); - } - throw typeError('string', frag.type); + return as( + data, + StringFragment, + (v: ValueFragmentInterface): v is StringFragmentInterface => + v.type === 'string', + StringFragment.from + ); } static from(frag: StringFragmentInterface): StringFragment { @@ -90,14 +89,13 @@ export class StringFragment { export class BooleanFragment { static as(data: unknown): BooleanFragment { - if (data instanceof BooleanFragment) { - return data; - } - const frag = ValueFragmentSchema.parse(data); - if (frag.type === 'boolean') { - return BooleanFragment.from(frag); - } - throw typeError('boolean', frag.type); + return as( + data, + BooleanFragment, + (v: ValueFragmentInterface): v is BooleanFragmentInterface => + v.type === 'boolean', + BooleanFragment.from + ); } static from(frag: BooleanFragmentInterface): BooleanFragment { @@ -114,14 +112,13 @@ export class BooleanFragment { export class ArrayFragment { static as(data: unknown): ArrayFragment { - if (data instanceof ArrayFragment) { - return data; - } - const frag = ValueFragmentSchema.parse(data); - if (frag.type === 'array') { - return ArrayFragment.from(frag); - } - throw typeError('array', frag.type); + return as( + data, + ArrayFragment, + (v: ValueFragmentInterface): v is ArrayFragmentInterface => + v.type === 'array', + ArrayFragment.from + ); } static from(frag: ArrayFragmentInterface): ArrayFragment { @@ -147,14 +144,13 @@ export class ArrayFragment { export class RecordFragment { static as(data: unknown): RecordFragment { - if (data instanceof RecordFragment) { - return data; - } - const frag = ValueFragmentSchema.parse(data); - if (frag.type !== 'record') { - throw typeError('record', frag.type); - } - return RecordFragment.from(frag); + return as( + data, + RecordFragment, + (v: ValueFragmentInterface): v is RecordFragmentInterface => + v.type === 'record', + RecordFragment.from + ); } static from(frag: RecordFragmentInterface): RecordFragment { @@ -241,8 +237,20 @@ export type Fragment = ValueFragment | AttributeFragment; // Static Functions -function typeError(expected: string, actual: string): Error { - return new Error(`Expected type ${expected}, but was ${actual}.`); +function as( + data: unknown, + fragCtor: new (...args: any[]) => F, + iTypeGuard: (v: ValueFragmentInterface) => v is I, + fromFn: (frag: I) => F +): F { + if (data instanceof fragCtor) { + return data; + } + const fragInterface = ValueFragmentSchema.parse(data); + if (iTypeGuard(fragInterface)) { + return fromFn(fragInterface); + } + throw new Error(`Incorrect value fragment type: ${fragInterface.type}.`); } export function asValue(data: unknown): ValueFragment { From 06952144f43856c8c05a471e0632e7dd550c9efb Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Tue, 9 May 2023 17:03:29 -0600 Subject: [PATCH 53/80] Test coverage to 100% --- .../manager/bazel-module/bazel-dep.spec.ts | 8 +++++++- .../manager/bazel-module/fragments.spec.ts | 15 ++++++++++++++- lib/modules/manager/bazel-module/fragments.ts | 4 ++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts index 8732dc88bd2604..d8d055456cbe28 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -3,7 +3,7 @@ import { toPackageDependency } from './bazel-dep'; import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; describe('modules/manager/bazel-module/bazel-dep', () => { - describe('BazelDepRecordToPackageDependency', () => { + describe('toPackageDependency()', () => { it('transforms a record fragment', () => { const record = new RecordFragment({ rule: new StringFragment('bazel_dep'), @@ -19,5 +19,11 @@ describe('modules/manager/bazel-module/bazel-dep', () => { currentValue: '1.2.3', }); }); + + it('returns null if not a record fragment', () => { + const frag = new StringFragment('hello'); + const result = toPackageDependency(frag); + expect(result).toBeNull(); + }); }); }); diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index ced0750a94c3c7..0ef6a5a01a2f6b 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -92,6 +92,12 @@ describe('modules/manager/bazel-module/fragments', () => { expect(record.children).toEqual({ name: new StringFragment('chicken') }); expect(record.isComplete).toBe(false); }); + + it('.safeAs() returns null with invalid data', () => { + expect( + RecordFragment.safeAs({ type: 'string', value: 'hello' }) + ).toBeNull(); + }); }); describe('AttributeFragment', () => { @@ -107,7 +113,7 @@ describe('modules/manager/bazel-module/fragments', () => { expect(attribute.isComplete).toBe(true); }); - describe('as', () => { + describe('.as()', () => { it('returns an AttributeFragment', () => { const frag = { type: 'attribute', @@ -118,6 +124,12 @@ describe('modules/manager/bazel-module/fragments', () => { expect(result).toEqual(frag); }); }); + + it('.safeAs() returns null with invalid data', () => { + expect( + AttributeFragment.safeAs({ type: 'string', value: 'hello' }) + ).toBeNull(); + }); }); describe('Fragments', () => { @@ -159,6 +171,7 @@ describe('modules/manager/bazel-module/fragments', () => { it.each` fn | frag ${StringFragment.as} | ${new ArrayFragment()} + ${BooleanFragment.as} | ${new ArrayFragment()} ${ArrayFragment.as} | ${new RecordFragment()} ${RecordFragment.as} | ${new ArrayFragment()} ${AttributeFragment.as} | ${new ArrayFragment()} diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 83d186739ad97f..3aff3e1b713d66 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -264,9 +264,9 @@ export function asValue(data: unknown): ValueFragment { return ArrayFragment.from(frag); case 'record': return RecordFragment.from(frag); - default: - throw new Error('Unexpected fragment type.'); } + // istanbul ignore next: catch new fragment type + throw new Error('Unexpected fragment type.'); } export function safeAsValue(data: unknown): ValueFragment | null { From 26b9c5c72005f68e0ea3096256e2af32cb2fd908 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 10 May 2023 08:52:47 -0600 Subject: [PATCH 54/80] Clarify error message --- lib/modules/manager/bazel-module/fragments.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 3aff3e1b713d66..b6e587861c4996 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -71,6 +71,7 @@ export class StringFragment { static as(data: unknown): StringFragment { return as( data, + 'string', StringFragment, (v: ValueFragmentInterface): v is StringFragmentInterface => v.type === 'string', @@ -91,6 +92,7 @@ export class BooleanFragment { static as(data: unknown): BooleanFragment { return as( data, + 'boolean', BooleanFragment, (v: ValueFragmentInterface): v is BooleanFragmentInterface => v.type === 'boolean', @@ -114,6 +116,7 @@ export class ArrayFragment { static as(data: unknown): ArrayFragment { return as( data, + 'array', ArrayFragment, (v: ValueFragmentInterface): v is ArrayFragmentInterface => v.type === 'array', @@ -146,6 +149,7 @@ export class RecordFragment { static as(data: unknown): RecordFragment { return as( data, + 'record', RecordFragment, (v: ValueFragmentInterface): v is RecordFragmentInterface => v.type === 'record', @@ -239,6 +243,7 @@ export type Fragment = ValueFragment | AttributeFragment; function as( data: unknown, + expected: string, fragCtor: new (...args: any[]) => F, iTypeGuard: (v: ValueFragmentInterface) => v is I, fromFn: (frag: I) => F @@ -250,7 +255,10 @@ function as( if (iTypeGuard(fragInterface)) { return fromFn(fragInterface); } - throw new Error(`Incorrect value fragment type: ${fragInterface.type}.`); + throw new Error( + 'The data is a value fragment, but it is not the expected type.' + + `expected: ${expected}, actual: ${fragInterface.type}.` + ); } export function asValue(data: unknown): ValueFragment { From 937643720f7e3bbb791b5c0d95cd6a3472656130 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 10 May 2023 13:06:07 -0600 Subject: [PATCH 55/80] Add filters back --- lib/modules/manager/bazel-module/extract.ts | 4 ++-- lib/modules/manager/bazel-module/filters.spec.ts | 12 ++++++++++++ lib/modules/manager/bazel-module/filters.ts | 7 +++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 lib/modules/manager/bazel-module/filters.spec.ts create mode 100644 lib/modules/manager/bazel-module/filters.ts diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index dd8e075b5f29d5..9f31059437df2d 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,6 +1,6 @@ -import is from '@sindresorhus/is'; import type { PackageFileContent } from '../types'; import * as bazelDep from './bazel-dep'; +import { isNotNullOrUndefined } from './filters'; import { parse } from './parser'; export function extractPackageFile( @@ -13,7 +13,7 @@ export function extractPackageFile( } const deps = fragments .map((frag) => bazelDep.toPackageDependency(frag)) - .filter(is.plainObject); + .filter(isNotNullOrUndefined); // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/filters.spec.ts b/lib/modules/manager/bazel-module/filters.spec.ts new file mode 100644 index 00000000000000..7afb387f89ae89 --- /dev/null +++ b/lib/modules/manager/bazel-module/filters.spec.ts @@ -0,0 +1,12 @@ +import { isNotNullOrUndefined } from './filters'; + +describe('modules/manager/bazel-module/filters', () => { + it.each` + a | exp + ${null} | ${false} + ${undefined} | ${false} + ${{ name: 'foo' }} | ${true} + `('.isNotNullOrUndefined', ({ a, exp }) => { + expect(isNotNullOrUndefined(a)).toEqual(exp); + }); +}); diff --git a/lib/modules/manager/bazel-module/filters.ts b/lib/modules/manager/bazel-module/filters.ts new file mode 100644 index 00000000000000..7ea8e458b9f49c --- /dev/null +++ b/lib/modules/manager/bazel-module/filters.ts @@ -0,0 +1,7 @@ +import is from '@sindresorhus/is'; + +export function isNotNullOrUndefined( + value: T | undefined | null +): value is T { + return !is.nullOrUndefined(value); +} From 2a92466b0cbb9b39f20bff1f404326104071e742 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 10 May 2023 14:56:24 -0600 Subject: [PATCH 56/80] Update toPackageDependency to resemble later implementation --- lib/modules/manager/bazel-module/bazel-dep.ts | 29 ++++++++++++---- lib/modules/manager/bazel-module/fragments.ts | 33 +++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index 23b9d2f3630080..7fe5ea516cdd60 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -1,7 +1,21 @@ -import is from '@sindresorhus/is'; +import { z } from 'zod'; import { BazelDatasource } from '../../datasource/bazel'; import type { PackageDependency } from '../types'; -import { RecordFragment, StringFragment, ValueFragment } from './fragments'; +import { + BooleanFragment, + RecordFragment, + StringFragment, + ValueFragment, +} from './fragments'; + +const BazelDepChildrenSchema = z.object({ + rule: StringFragment.schema.extend({ + value: z.literal('bazel_dep'), + }), + name: StringFragment.schema, + version: StringFragment.schema, + dev_dependency: BooleanFragment.schema.optional(), +}); export function toPackageDependency( value: ValueFragment @@ -10,14 +24,15 @@ export function toPackageDependency( if (!record) { return null; } - const { rule, name, version } = record.children; - if (is.falsy(rule) || is.falsy(name) || is.falsy(version)) { + const parseResult = BazelDepChildrenSchema.safeParse(record.children); + if (!parseResult.success) { return null; } + const { rule, name, version } = parseResult.data; return { datasource: BazelDatasource.id, - depType: StringFragment.as(rule).value, - depName: StringFragment.as(name).value, - currentValue: StringFragment.as(version).value, + depType: rule.value, + depName: name.value, + currentValue: version.value, }; } diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index b6e587861c4996..4c568016b6267d 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -52,6 +52,29 @@ export const ValueFragmentSchema: z.ZodType = z.lazy( ]) ); +// NOTE: The XXXFragmentSchema definitions must be kept in-sync with the +// schemas defined in ValueFragmentSchema. Ideally, we would define each schema +// in one place and reference it elsewhere. Unfortunately, type errors occur +// if we use the standalone schemas in the the discriminatedUnion declaration. + +const StringFragmentSchema = z.object({ + type: z.literal('string'), + value: z.string(), +}); +const BooleanFragmentSchema = z.object({ + type: z.literal('boolean'), + value: z.boolean(), +}); +const ArrayFragmentSchema = z.object({ + type: z.literal('array'), + items: ValueFragmentSchema.array(), + isComplete: z.boolean(), +}); +const RecordFragmentSchema = z.object({ + type: z.literal('record'), + children: z.record(z.string(), ValueFragmentSchema), + isComplete: z.boolean(), +}); const AttributeFragmentSchema = z.object({ type: z.literal('attribute'), name: z.string(), @@ -68,6 +91,8 @@ export type FragmentType = | 'attribute'; export class StringFragment { + static readonly schema = StringFragmentSchema; + static as(data: unknown): StringFragment { return as( data, @@ -89,6 +114,8 @@ export class StringFragment { } export class BooleanFragment { + static readonly schema = BooleanFragmentSchema; + static as(data: unknown): BooleanFragment { return as( data, @@ -113,6 +140,8 @@ export class BooleanFragment { } export class ArrayFragment { + static readonly schema = ArrayFragmentSchema; + static as(data: unknown): ArrayFragment { return as( data, @@ -146,6 +175,8 @@ export class ArrayFragment { } export class RecordFragment { + static readonly schema = RecordFragmentSchema; + static as(data: unknown): RecordFragment { return as( data, @@ -194,6 +225,8 @@ export class RecordFragment { } export class AttributeFragment { + static readonly schema = AttributeFragmentSchema; + static as(data: unknown): AttributeFragment { if (data instanceof AttributeFragment) { return data; From 12cfe5f49cd84cf2a424376d51e5915b79c339e6 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 10 May 2023 16:28:27 -0600 Subject: [PATCH 57/80] Reimplement Ctx.as. --- lib/modules/manager/bazel-module/context.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 79fac7ce1b99cd..6b465173b3924b 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -19,19 +19,24 @@ export interface CtxCompatible { } export class Ctx implements CtxCompatible { - results: ValueFragment[] = []; - stack = Stack.create(); + results: ValueFragment[]; + stack: Stack; + + constructor(results: ValueFragment[] = [], stack = Stack.create()) { + this.results = results; + this.stack = stack; + } // This exists because the good-enough-parser gives a cloned instance of our // Ctx. It is missing the Ctx prototype. This function adds the proper prototype // to the context and the items referenced by the context. static as(obj: CtxCompatible): Ctx { - Object.setPrototypeOf(obj, Ctx.prototype); - const ctx = obj as Ctx; - const stackItems = ctx.stack.map((item) => asFragment(item)); - ctx.stack = Stack.create(...stackItems); - ctx.results = ctx.results.map((item) => RecordFragment.as(item)); - return ctx; + if (obj instanceof Ctx) { + return obj; + } + const results = obj.results.map((frag) => RecordFragment.as(frag)); + const stackItems = obj.stack.map((frag) => asFragment(frag)); + return new Ctx(results, Stack.create(...stackItems)); } get currentRecord(): RecordFragment { From 712c98f811e2323db869b1ca443a4d715478f17c Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Wed, 10 May 2023 16:29:56 -0600 Subject: [PATCH 58/80] Update import --- lib/modules/manager/bazel-module/extract.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 9f31059437df2d..7f44e1a5ec0135 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,5 +1,5 @@ import type { PackageFileContent } from '../types'; -import * as bazelDep from './bazel-dep'; +import { toPackageDependency } from './bazel-dep'; import { isNotNullOrUndefined } from './filters'; import { parse } from './parser'; @@ -12,7 +12,7 @@ export function extractPackageFile( return null; } const deps = fragments - .map((frag) => bazelDep.toPackageDependency(frag)) + .map((frag) => toPackageDependency(frag)) .filter(isNotNullOrUndefined); // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; From 21740876b57352859b517059fe9aea3754930eeb Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 11 May 2023 10:18:00 -0600 Subject: [PATCH 59/80] Use isNotNullOrUndefined from lib/util/array --- lib/modules/manager/bazel-module/extract.ts | 2 +- lib/modules/manager/bazel-module/filters.spec.ts | 12 ------------ lib/modules/manager/bazel-module/filters.ts | 7 ------- 3 files changed, 1 insertion(+), 20 deletions(-) delete mode 100644 lib/modules/manager/bazel-module/filters.spec.ts delete mode 100644 lib/modules/manager/bazel-module/filters.ts diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 7f44e1a5ec0135..b46c12c6368c12 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,6 +1,6 @@ +import { isNotNullOrUndefined } from '../../../util/array'; import type { PackageFileContent } from '../types'; import { toPackageDependency } from './bazel-dep'; -import { isNotNullOrUndefined } from './filters'; import { parse } from './parser'; export function extractPackageFile( diff --git a/lib/modules/manager/bazel-module/filters.spec.ts b/lib/modules/manager/bazel-module/filters.spec.ts deleted file mode 100644 index 7afb387f89ae89..00000000000000 --- a/lib/modules/manager/bazel-module/filters.spec.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { isNotNullOrUndefined } from './filters'; - -describe('modules/manager/bazel-module/filters', () => { - it.each` - a | exp - ${null} | ${false} - ${undefined} | ${false} - ${{ name: 'foo' }} | ${true} - `('.isNotNullOrUndefined', ({ a, exp }) => { - expect(isNotNullOrUndefined(a)).toEqual(exp); - }); -}); diff --git a/lib/modules/manager/bazel-module/filters.ts b/lib/modules/manager/bazel-module/filters.ts deleted file mode 100644 index 7ea8e458b9f49c..00000000000000 --- a/lib/modules/manager/bazel-module/filters.ts +++ /dev/null @@ -1,7 +0,0 @@ -import is from '@sindresorhus/is'; - -export function isNotNullOrUndefined( - value: T | undefined | null -): value is T { - return !is.nullOrUndefined(value); -} From d97f05c26eb0e2928e7d7770a3da657b2fdd1b51 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 11 May 2023 12:30:00 -0600 Subject: [PATCH 60/80] Rework parse and extract to avoid null value until the end of extractPackageFile --- lib/modules/manager/bazel-module/extract.ts | 7 +------ lib/modules/manager/bazel-module/parser.ts | 10 +++------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index b46c12c6368c12..d2b82acea95752 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -7,13 +7,8 @@ export function extractPackageFile( content: string, packageFile: string ): PackageFileContent | null { - const fragments = parse(content, packageFile); - if (!fragments) { - return null; - } - const deps = fragments + const deps = parse(content, packageFile) .map((frag) => toPackageDependency(frag)) .filter(isNotNullOrUndefined); - // istanbul ignore next: cannot reach null without introducing fake rule return deps.length ? { deps } : null; } diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 6e9f6f5e72e26f..2ebb6b8c88d888 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -53,18 +53,14 @@ const query = q.tree({ const starlarkLang = lang.createLang('starlark'); -export function parse( - input: string, - packageFile?: string -): ValueFragment[] | null { - let result: ValueFragment[] | null = null; +export function parse(input: string, packageFile?: string): ValueFragment[] { try { const parsedResult = starlarkLang.query(input, query, new Ctx()); if (parsedResult) { - result = parsedResult.results; + return parsedResult.results; } } catch (err) /* istanbul ignore next */ { logger.debug({ err, packageFile }, 'Bazel module parsing error'); } - return result; + return []; } From 9b3f1940f294c778824d69ff49a2d8200c201d14 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 11 May 2023 12:42:47 -0600 Subject: [PATCH 61/80] Rename kwParams to kvParams --- lib/modules/manager/bazel-module/parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 2ebb6b8c88d888..f71d89f058a1f6 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -14,7 +14,7 @@ const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); * - `name = "foobar"` * - `dev_dependeny = True` **/ -const kwParams = q +const kvParams = q .sym((ctx, token) => { return Ctx.as(ctx).startAttribute(token.value); }) @@ -36,7 +36,7 @@ const moduleRules = q q.tree({ type: 'wrapped-tree', maxDepth: 1, - search: kwParams, + search: kvParams, postHandler: (ctx, tree) => { return Ctx.as(ctx).endRule(); }, From 0da45f572dfd203d18fe9eaee3f4692b10559e2d Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 11 May 2023 12:55:24 -0600 Subject: [PATCH 62/80] Remove try-catch in parser as it appears to be not needed --- lib/modules/manager/bazel-module/parser.spec.ts | 10 ++++++++++ lib/modules/manager/bazel-module/parser.ts | 11 +++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index 8530fbc248913e..e7b4687a24f6f3 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -4,6 +4,16 @@ import { parse } from './parser'; describe('modules/manager/bazel-module/parser', () => { describe('parse', () => { + it('returns empty string if invalid content', () => { + const input = codeBlock` + // This is invalid + a + 1 + <<<<<<< + `; + const res = parse(input, 'MODULE.bazel'); + expect(res).toHaveLength(0); + }); + it('finds simple bazel_dep', () => { const input = codeBlock` bazel_dep(name = "rules_foo", version = "1.2.3") diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index f71d89f058a1f6..691ab94e94e9ea 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -1,5 +1,4 @@ import { lang, query as q } from 'good-enough-parser'; -import { logger } from '../../../logger'; import { regEx } from '../../../util/regex'; import { Ctx } from './context'; import type { ValueFragment } from './fragments'; @@ -54,13 +53,9 @@ const query = q.tree({ const starlarkLang = lang.createLang('starlark'); export function parse(input: string, packageFile?: string): ValueFragment[] { - try { - const parsedResult = starlarkLang.query(input, query, new Ctx()); - if (parsedResult) { - return parsedResult.results; - } - } catch (err) /* istanbul ignore next */ { - logger.debug({ err, packageFile }, 'Bazel module parsing error'); + const parsedResult = starlarkLang.query(input, query, new Ctx()); + if (parsedResult) { + return parsedResult.results; } return []; } From 7a10ec483bffcf65d075303a06df1d5357236c74 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 11 May 2023 12:59:33 -0600 Subject: [PATCH 63/80] Fix check for undefined in Stack.current() --- lib/modules/manager/bazel-module/stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/stack.ts b/lib/modules/manager/bazel-module/stack.ts index c0fd8f3e3d57f4..d9c5ceeee884b5 100644 --- a/lib/modules/manager/bazel-module/stack.ts +++ b/lib/modules/manager/bazel-module/stack.ts @@ -14,7 +14,7 @@ export class Stack extends Array { get current(): T { const c = this.safeCurrent; - if (!c) { + if (c === undefined) { throw new Error('Requested current, but no value.'); } return c; From 978f0eb8080d3053eb581bd8d5021e61d668f747 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 08:38:14 -0600 Subject: [PATCH 64/80] Remove unused parameter from parse signature --- lib/modules/manager/bazel-module/extract.ts | 2 +- lib/modules/manager/bazel-module/parser.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index d2b82acea95752..99976cfa865ca6 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -7,7 +7,7 @@ export function extractPackageFile( content: string, packageFile: string ): PackageFileContent | null { - const deps = parse(content, packageFile) + const deps = parse(content) .map((frag) => toPackageDependency(frag)) .filter(isNotNullOrUndefined); return deps.length ? { deps } : null; diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 691ab94e94e9ea..a31866383d4f37 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -52,7 +52,7 @@ const query = q.tree({ const starlarkLang = lang.createLang('starlark'); -export function parse(input: string, packageFile?: string): ValueFragment[] { +export function parse(input: string): ValueFragment[] { const parsedResult = starlarkLang.query(input, query, new Ctx()); if (parsedResult) { return parsedResult.results; From 03b15bda4153a916e4b903840b6dbdb066ad1c34 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 08:41:08 -0600 Subject: [PATCH 65/80] Fix parser.spec --- lib/modules/manager/bazel-module/parser.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index e7b4687a24f6f3..d307eb90174c12 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -10,7 +10,7 @@ describe('modules/manager/bazel-module/parser', () => { a + 1 <<<<<<< `; - const res = parse(input, 'MODULE.bazel'); + const res = parse(input); expect(res).toHaveLength(0); }); @@ -19,7 +19,7 @@ describe('modules/manager/bazel-module/parser', () => { bazel_dep(name = "rules_foo", version = "1.2.3") bazel_dep(name = "rules_bar", version = "1.0.0", dev_dependency = True) `; - const res = parse(input, 'MODULE.bazel'); + const res = parse(input); expect(res).toEqual([ new RecordFragment( { From 39cee4b2c2f4c5e6064b4ebfb1f7999e9e9a5df1 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 09:33:01 -0600 Subject: [PATCH 66/80] Add log message if a package file does not provide any package dependencies --- lib/modules/manager/bazel-module/extract.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 99976cfa865ca6..351aca2bc640e5 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,3 +1,4 @@ +import { logger } from '../../../logger'; import { isNotNullOrUndefined } from '../../../util/array'; import type { PackageFileContent } from '../types'; import { toPackageDependency } from './bazel-dep'; @@ -10,5 +11,12 @@ export function extractPackageFile( const deps = parse(content) .map((frag) => toPackageDependency(frag)) .filter(isNotNullOrUndefined); - return deps.length ? { deps } : null; + if (deps.length) { + return { deps }; + } + logger.debug( + { packageFile }, + 'The package file did not contain any package dependencies.' + ); + return null; } From ff894e567a89fbdec5562ec4266dfae87e7fcc30 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 15:35:49 -0600 Subject: [PATCH 67/80] Tests are green after reworking fragments removing classes --- .../manager/bazel-module/bazel-dep.spec.ts | 16 +- lib/modules/manager/bazel-module/bazel-dep.ts | 37 +- .../manager/bazel-module/context.spec.ts | 41 +- lib/modules/manager/bazel-module/context.ts | 99 ++--- .../manager/bazel-module/extract.spec.ts | 9 - .../manager/bazel-module/fragments.spec.ts | 222 +++------- lib/modules/manager/bazel-module/fragments.ts | 381 ++++-------------- .../manager/bazel-module/parser.spec.ts | 20 +- lib/modules/manager/bazel-module/parser.ts | 4 +- 9 files changed, 236 insertions(+), 593 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts index d8d055456cbe28..d4a521dc128a61 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -1,15 +1,15 @@ import { BazelDatasource } from '../../datasource/bazel'; import { toPackageDependency } from './bazel-dep'; -import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; +import * as fragments from './fragments'; describe('modules/manager/bazel-module/bazel-dep', () => { describe('toPackageDependency()', () => { it('transforms a record fragment', () => { - const record = new RecordFragment({ - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), - dev_dependency: new BooleanFragment(true), + const record = fragments.record({ + rule: fragments.string('bazel_dep'), + name: fragments.string('rules_foo'), + version: fragments.string('1.2.3'), + dev_dependency: fragments.boolean(true), }); const result = toPackageDependency(record); expect(result).toEqual({ @@ -20,8 +20,8 @@ describe('modules/manager/bazel-module/bazel-dep', () => { }); }); - it('returns null if not a record fragment', () => { - const frag = new StringFragment('hello'); + it('returns null if invalid', () => { + const frag = fragments.record(); const result = toPackageDependency(frag); expect(result).toBeNull(); }); diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index 7fe5ea516cdd60..3b5445761fc3e7 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -2,37 +2,32 @@ import { z } from 'zod'; import { BazelDatasource } from '../../datasource/bazel'; import type { PackageDependency } from '../types'; import { - BooleanFragment, + BooleanFragmentSchema, RecordFragment, - StringFragment, - ValueFragment, + StringFragmentSchema, } from './fragments'; const BazelDepChildrenSchema = z.object({ - rule: StringFragment.schema.extend({ + rule: StringFragmentSchema.extend({ value: z.literal('bazel_dep'), }), - name: StringFragment.schema, - version: StringFragment.schema, - dev_dependency: BooleanFragment.schema.optional(), + name: StringFragmentSchema, + version: StringFragmentSchema, + dev_dependency: BooleanFragmentSchema.optional(), }); -export function toPackageDependency( - value: ValueFragment -): PackageDependency | null { - const record = RecordFragment.safeAs(value); - if (!record) { - return null; - } - const parseResult = BazelDepChildrenSchema.safeParse(record.children); - if (!parseResult.success) { - return null; - } - const { rule, name, version } = parseResult.data; - return { +const ToBazelDep = BazelDepChildrenSchema.transform( + ({ rule, name, version }): PackageDependency => ({ datasource: BazelDatasource.id, depType: rule.value, depName: name.value, currentValue: version.value, - }; + }) +); + +export function toPackageDependency( + record: RecordFragment +): PackageDependency | null { + const parseResult = ToBazelDep.safeParse(record.children); + return parseResult.success ? parseResult.data : null; } diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index 384a4e9501ae71..ec33564f5180d1 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -1,5 +1,5 @@ -import { Ctx } from './context'; -import { ArrayFragment, RecordFragment, StringFragment } from './fragments'; +import { Ctx, CtxProcessingError } from './context'; +import * as fragments from './fragments'; describe('modules/manager/bazel-module/context', () => { describe('Ctx', () => { @@ -13,11 +13,11 @@ describe('modules/manager/bazel-module/context', () => { .endRule(); expect(ctx.results).toEqual([ - new RecordFragment( + fragments.record( { - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), + rule: fragments.string('bazel_dep'), + name: fragments.string('rules_foo'), + version: fragments.string('1.2.3'), }, true ), @@ -31,25 +31,18 @@ describe('modules/manager/bazel-module/context', () => { .addString('my_library') .startAttribute('srcs') .startArray() - .addArrayItem('first') - .addArrayItem('second') + .addString('first') + .addString('second') .endArray() - .startAttribute('tags') - .startRule('get_tags') - .endRule() .endRule(); expect(ctx.results).toEqual([ - new RecordFragment( + fragments.record( { - rule: new StringFragment('foo_library'), - name: new StringFragment('my_library'), - srcs: new ArrayFragment( - [new StringFragment('first'), new StringFragment('second')], - true - ), - tags: new RecordFragment( - { rule: new StringFragment('get_tags') }, + rule: fragments.string('foo_library'), + name: fragments.string('my_library'), + srcs: fragments.array( + [fragments.string('first'), fragments.string('second')], true ), }, @@ -61,7 +54,7 @@ describe('modules/manager/bazel-module/context', () => { describe('currentRecord', () => { it('returns the record fragment if it is current', () => { const ctx = new Ctx().startRecord(); - expect(ctx.currentRecord).toEqual(new RecordFragment()); + expect(ctx.currentRecord).toEqual(fragments.record()); }); it('throws if the current is not a record fragment', () => { @@ -75,7 +68,7 @@ describe('modules/manager/bazel-module/context', () => { describe('currentArray', () => { it('returns the array fragment if it is current', () => { const ctx = new Ctx().startArray(); - expect(ctx.currentArray).toEqual(new ArrayFragment()); + expect(ctx.currentArray).toEqual(fragments.array()); }); it('throws if the current is not a record fragment', () => { @@ -89,7 +82,9 @@ describe('modules/manager/bazel-module/context', () => { it('throws if add an attribute without a parent', () => { const ctx = new Ctx().startAttribute('name'); expect(() => ctx.addString('chicken')).toThrow( - new Error('Processing an attribute but there is no parent.') + new CtxProcessingError( + fragments.attribute('name', fragments.string('chicken')) + ) ); }); }); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 6b465173b3924b..72b5d2c3c15816 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -1,47 +1,51 @@ -import { +import type { + AllFragments, ArrayFragment, - AttributeFragment, - BooleanFragment, ChildFragments, - Fragment, RecordFragment, - StringFragment, - ValueFragment, - asFragment, - safeAsValue, } from './fragments'; +import * as fragments from './fragments'; import { Stack } from './stack'; // Represents the fields that the context must have. export interface CtxCompatible { - results: ValueFragment[]; - stack: Stack; + results: RecordFragment[]; + stack: Stack; +} + +export class CtxProcessingError extends Error { + readonly current: AllFragments; + readonly parent?: AllFragments; + constructor(current: AllFragments, parent?: AllFragments) { + super(); + this.name = 'CtxProcessingError'; + this.current = current; + this.parent = parent; + } } export class Ctx implements CtxCompatible { - results: ValueFragment[]; - stack: Stack; + results: RecordFragment[]; + stack: Stack; - constructor(results: ValueFragment[] = [], stack = Stack.create()) { + constructor( + results: RecordFragment[] = [], + stack = Stack.create() + ) { this.results = results; this.stack = stack; } - // This exists because the good-enough-parser gives a cloned instance of our - // Ctx. It is missing the Ctx prototype. This function adds the proper prototype - // to the context and the items referenced by the context. static as(obj: CtxCompatible): Ctx { if (obj instanceof Ctx) { return obj; } - const results = obj.results.map((frag) => RecordFragment.as(frag)); - const stackItems = obj.stack.map((frag) => asFragment(frag)); - return new Ctx(results, Stack.create(...stackItems)); + return new Ctx(obj.results, Stack.create(...obj.stack)); } get currentRecord(): RecordFragment { const current = this.stack.current; - if (current instanceof RecordFragment) { + if (current.type === 'record') { return current; } throw new Error('Requested current record, but does not exist.'); @@ -49,7 +53,7 @@ export class Ctx implements CtxCompatible { get currentArray(): ArrayFragment { const current = this.stack.current; - if (current instanceof ArrayFragment) { + if (current.type === 'array') { return current; } throw new Error('Requested current array, but does not exist.'); @@ -57,34 +61,37 @@ export class Ctx implements CtxCompatible { private popStack(): void { const current = this.stack.pop(); + // TODO: Try to remove this istanbul comment. // istanbul ignore if: we should never get here if (!current) { return; } const parent = this.stack.safeCurrent; - const value = safeAsValue(current); - if (value) { - if (parent && 'addValue' in parent) { - parent.addValue(value); + + if (parent) { + if (parent.type === 'attribute' && fragments.isValue(current)) { + parent.value = current; + parent.isComplete = true; return; } - if (!parent) { - this.results.push(value); + if (parent.type === 'array' && fragments.isPrimitive(current)) { + parent.items.push(current); return; } - } - const attribute = AttributeFragment.safeAs(current); - if (attribute) { - if (parent && 'addAttribute' in parent) { - parent.addAttribute(attribute); + if ( + parent.type === 'record' && + current.type === 'attribute' && + current.value !== undefined + ) { + parent.children[current.name] = current.value; return; } - if (!parent) { - throw new Error('Processing an attribute but there is no parent.'); - } + } else if (current.type === 'record') { + this.results.push(current); + return; } - // istanbul ignore next: catching future mistakes - throw new Error('We are in a bad place.'); + + throw new CtxProcessingError(current, parent); } private processStack(): Ctx { @@ -97,17 +104,17 @@ export class Ctx implements CtxCompatible { } addString(value: string): Ctx { - this.stack.push(new StringFragment(value)); + this.stack.push(fragments.string(value)); return this.processStack(); } addBoolean(value: string | boolean): Ctx { - this.stack.push(new BooleanFragment(value)); + this.stack.push(fragments.boolean(value)); return this.processStack(); } startRecord(children: ChildFragments = {}): Ctx { - const record = new RecordFragment(children); + const record = fragments.record(children); this.stack.push(record); return this; } @@ -119,7 +126,7 @@ export class Ctx implements CtxCompatible { } startRule(name: string): Ctx { - return this.startRecord({ rule: new StringFragment(name) }); + return this.startRecord({ rule: fragments.string(name) }); } endRule(): Ctx { @@ -127,21 +134,15 @@ export class Ctx implements CtxCompatible { } startAttribute(name: string): Ctx { - this.stack.push(new AttributeFragment(name)); + this.stack.push(fragments.attribute(name)); return this.processStack(); } startArray(): Ctx { - this.stack.push(new ArrayFragment()); + this.stack.push(fragments.array()); return this.processStack(); } - addArrayItem(value: string): Ctx { - const array = this.currentArray; - array.items.push(new StringFragment(value)); - return this; - } - endArray(): Ctx { const array = this.currentArray; array.isComplete = true; diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index de53bc9925f437..6182e57424b481 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -1,7 +1,5 @@ import { codeBlock } from 'common-tags'; import { BazelDatasource } from '../../datasource/bazel'; -import { RecordFragment, ValueFragment } from './fragments'; -import * as parser from './parser'; import { extractPackageFile } from '.'; describe('modules/manager/bazel-module/extract', () => { @@ -24,13 +22,6 @@ describe('modules/manager/bazel-module/extract', () => { expect(result).toBeNull(); }); - it('gracefully handles unexpected fragments', () => { - const fragments: ValueFragment[] = [new RecordFragment()]; - const mockParse = jest.spyOn(parser, 'parse'); - mockParse.mockReturnValueOnce(fragments); - expect(extractPackageFile('', 'MODULE.bazel')).toBeNull(); - }); - it('returns dependencies', () => { const input = codeBlock` bazel_dep(name = "rules_foo", version = "1.2.3") diff --git a/lib/modules/manager/bazel-module/fragments.spec.ts b/lib/modules/manager/bazel-module/fragments.spec.ts index 0ef6a5a01a2f6b..bf30857ceef00f 100644 --- a/lib/modules/manager/bazel-module/fragments.spec.ts +++ b/lib/modules/manager/bazel-module/fragments.spec.ts @@ -1,192 +1,64 @@ import { - ArrayFragment, - AttributeFragment, - BooleanFragment, - RecordFragment, - StringFragment, - asFragment, - asValue, - safeAsValue, + ArrayFragmentSchema, + AttributeFragmentSchema, + BooleanFragmentSchema, + RecordFragmentSchema, + StringFragmentSchema, } from './fragments'; +import * as fragments from './fragments'; describe('modules/manager/bazel-module/fragments', () => { - describe('StringFragment', () => { - it('lifecycle ', () => { - const string = new StringFragment('hello'); - expect(string.value).toBe('hello'); - expect(string.isComplete).toBe(true); - }); + it('.string()', () => { + const result = fragments.string('hello'); + expect(() => StringFragmentSchema.parse(result)).not.toThrow(); + expect(result.value).toBe('hello'); }); - describe('BooleanFragment', () => { - it('lifecycle ', () => { - const bool = new BooleanFragment(true); - expect(bool.value).toBe(true); - expect(bool.isComplete).toBe(true); - }); - - it.each` - a | exp - ${true} | ${true} - ${false} | ${false} - ${'True'} | ${true} - ${'False'} | ${false} - `('new BooleanFragment($a)', ({ a, exp }) => { - const bool = new BooleanFragment(a); - expect(bool.value).toBe(exp); - }); - - it.each` - a - ${'true'} - ${'false'} - ${''} - `('new BooleanFragment($a)', ({ a }) => { - expect(() => new BooleanFragment(a)).toThrow(); - }); + it('.boolean()', () => { + const result = fragments.boolean(true); + expect(() => BooleanFragmentSchema.parse(result)).not.toThrow(); + expect(result.value).toBe(true); }); - describe('ArrayFragment', () => { - it('lifecycle', () => { - const array = new ArrayFragment(); - expect(array.items).toHaveLength(0); - expect(array.isComplete).toBe(false); - - array.addValue(new StringFragment('hello')); - expect(array.items).toEqual([new StringFragment('hello')]); - expect(array.isComplete).toBe(false); - }); - - describe('as', () => { - it('returns an ArrayFragment', () => { - const frag = { - type: 'array', - isComplete: false, - items: [new StringFragment('hello')], - }; - const result = ArrayFragment.as(frag); - expect(result).toEqual(frag); - expect(result).toBeInstanceOf(ArrayFragment); - }); - }); + 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') }); + expect(result.isComplete).toBe(true); }); - describe('RecordFragment', () => { - it('lifecycle', () => { - const record = new RecordFragment(); - expect(record.children).toEqual({}); - expect(record.isComplete).toBe(false); - - const attribute = new AttributeFragment( - 'name', - new StringFragment('chicken') - ); - record.addAttribute(attribute); - expect(record.children).toEqual({ name: new StringFragment('chicken') }); - expect(record.isComplete).toBe(false); - - const badAttribute = new AttributeFragment('type'); - expect(() => record.addAttribute(badAttribute)).toThrow( - new Error('The attribute fragment does not have a value.') - ); - expect(record.children).toEqual({ name: new StringFragment('chicken') }); - expect(record.isComplete).toBe(false); - }); - - it('.safeAs() returns null with invalid data', () => { - expect( - RecordFragment.safeAs({ type: 'string', value: 'hello' }) - ).toBeNull(); - }); + it('.attribute()', () => { + const result = fragments.attribute('name', fragments.string('foo'), true); + expect(() => AttributeFragmentSchema.parse(result)).not.toThrow(); + expect(result.name).toBe('name'); + expect(result.value).toEqual(fragments.string('foo')); + expect(result.isComplete).toBe(true); }); - describe('AttributeFragment', () => { - it('lifecycle', () => { - const attribute = new AttributeFragment('name'); - expect(attribute.name).toBe('name'); - expect(attribute.value).toBeUndefined(); - expect(attribute.isComplete).toBe(false); - - const value = new StringFragment('chicken'); - attribute.addValue(value); - expect(attribute.value).toEqual(value); - expect(attribute.isComplete).toBe(true); - }); - - describe('.as()', () => { - it('returns an AttributeFragment', () => { - const frag = { - type: 'attribute', - name: 'greeting', - value: new StringFragment('hello'), - }; - const result = AttributeFragment.as(frag); - expect(result).toEqual(frag); - }); - }); - - it('.safeAs() returns null with invalid data', () => { - expect( - AttributeFragment.safeAs({ type: 'string', value: 'hello' }) - ).toBeNull(); - }); + it('.array()', () => { + const result = fragments.array([fragments.string('foo')], true); + expect(() => ArrayFragmentSchema.parse(result)).not.toThrow(); + expect(result.items).toEqual([fragments.string('foo')]); + expect(result.isComplete).toBe(true); }); - describe('Fragments', () => { - it.each` - frag | exp - ${new StringFragment('hello')} | ${new StringFragment('hello')} - ${new ArrayFragment()} | ${new ArrayFragment()} - ${new RecordFragment()} | ${new RecordFragment()} - ${new AttributeFragment('name')} | ${null} - `('safeAsValue($frag)', ({ frag, exp }) => { - const result = safeAsValue(frag); - expect(result).toEqual(exp); - }); - - describe('asValue', () => { - it('returns the instance when it is a value fragment', () => { - const value = new StringFragment('hello'); - const result = asValue(value); - expect(result).toEqual(value); - }); - - it('throws when it is not a value fragment', () => { - const attribute = new AttributeFragment('name'); - expect(() => asValue(attribute)).toThrow(); - }); - }); - - it.each` - frag | exp - ${new StringFragment('hello')} | ${new StringFragment('hello')} - ${new ArrayFragment()} | ${new ArrayFragment()} - ${new RecordFragment()} | ${new RecordFragment()} - ${new AttributeFragment('name')} | ${new AttributeFragment('name')} - `('asFragment($frag)', ({ frag, exp }) => { - const result = asFragment(frag); - expect(result).toEqual(exp); - }); - - it.each` - fn | frag - ${StringFragment.as} | ${new ArrayFragment()} - ${BooleanFragment.as} | ${new ArrayFragment()} - ${ArrayFragment.as} | ${new RecordFragment()} - ${RecordFragment.as} | ${new ArrayFragment()} - ${AttributeFragment.as} | ${new ArrayFragment()} - `('$fn throws with $frag', ({ fn, frag }) => { - expect(() => fn(frag)).toThrow(); - }); + it.each` + a | exp + ${fragments.string('hello')} | ${true} + ${fragments.boolean(true)} | ${true} + ${fragments.array()} | ${true} + ${fragments.record()} | ${false} + `('.isValue($a)', ({ a, exp }) => { + expect(fragments.isValue(a)).toBe(exp); + }); - it.each` - fn | data | exp - ${StringFragment.as} | ${{ type: 'string', value: 'hello' }} | ${new StringFragment('hello')} - ${BooleanFragment.as} | ${{ type: 'boolean', value: true }} | ${new BooleanFragment(true)} - ${ArrayFragment.as} | ${{ type: 'array', isComplete: false, items: [] }} | ${new ArrayFragment()} - ${RecordFragment.as} | ${{ type: 'record', isComplete: false, children: {} }} | ${new RecordFragment()} - `('$fn with $data', ({ fn, data, exp }) => { - expect(fn(data)).toEqual(exp); - }); + it.each` + a | exp + ${fragments.string('hello')} | ${true} + ${fragments.boolean(true)} | ${true} + ${fragments.array()} | ${false} + ${fragments.record()} | ${false} + `('.isValue($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 4c568016b6267d..5237cb06ec7600 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -1,327 +1,116 @@ -import is from '@sindresorhus/is'; import { z } from 'zod'; import * as starlark from './starlark'; -// Fragment Schemas - -interface StringFragmentInterface { - readonly type: 'string'; - readonly value: string; -} - -interface BooleanFragmentInterface { - readonly type: 'boolean'; - readonly value: boolean; -} - -interface ArrayFragmentInterface { - readonly type: 'array'; - items: ValueFragmentInterface[]; - isComplete: boolean; -} - -type ChildFragmentsInterface = Record; - -interface RecordFragmentInterface { - readonly type: 'record'; - children: ChildFragmentsInterface; - isComplete: boolean; -} - -type ValueFragmentInterface = - | StringFragmentInterface - | BooleanFragmentInterface - | ArrayFragmentInterface - | RecordFragmentInterface; - -export const ValueFragmentSchema: z.ZodType = z.lazy( - () => - z.discriminatedUnion('type', [ - z.object({ type: z.literal('string'), value: z.string() }), - z.object({ type: z.literal('boolean'), value: z.boolean() }), - z.object({ - type: z.literal('array'), - items: ValueFragmentSchema.array(), - isComplete: z.boolean(), - }), - z.object({ - type: z.literal('record'), - children: z.record(z.string(), ValueFragmentSchema), - isComplete: z.boolean(), - }), - ]) -); - -// NOTE: The XXXFragmentSchema definitions must be kept in-sync with the -// schemas defined in ValueFragmentSchema. Ideally, we would define each schema -// in one place and reference it elsewhere. Unfortunately, type errors occur -// if we use the standalone schemas in the the discriminatedUnion declaration. - -const StringFragmentSchema = z.object({ +export const StringFragmentSchema = z.object({ type: z.literal('string'), value: z.string(), + isComplete: z.literal(true), }); -const BooleanFragmentSchema = z.object({ +export const BooleanFragmentSchema = z.object({ type: z.literal('boolean'), value: z.boolean(), + isComplete: z.literal(true), }); -const ArrayFragmentSchema = z.object({ +const PrimitiveFragmentsSchema = z.discriminatedUnion('type', [ + StringFragmentSchema, + BooleanFragmentSchema, +]); +export const ArrayFragmentSchema = z.object({ type: z.literal('array'), - items: ValueFragmentSchema.array(), + items: PrimitiveFragmentsSchema.array(), isComplete: z.boolean(), }); -const RecordFragmentSchema = z.object({ +const ValueFragmentsSchema = z.discriminatedUnion('type', [ + StringFragmentSchema, + BooleanFragmentSchema, + ArrayFragmentSchema, +]); +export const RecordFragmentSchema = z.object({ type: z.literal('record'), - children: z.record(z.string(), ValueFragmentSchema), + children: z.record(z.string(), ValueFragmentsSchema), isComplete: z.boolean(), }); -const AttributeFragmentSchema = z.object({ +export const AttributeFragmentSchema = z.object({ type: z.literal('attribute'), name: z.string(), - value: ValueFragmentSchema.optional(), + value: ValueFragmentsSchema.optional(), + isComplete: z.boolean(), }); - -// Fragment Types - -export type FragmentType = - | 'string' - | 'boolean' - | 'array' - | 'record' - | 'attribute'; - -export class StringFragment { - static readonly schema = StringFragmentSchema; - - static as(data: unknown): StringFragment { - return as( - data, - 'string', - StringFragment, - (v: ValueFragmentInterface): v is StringFragmentInterface => - v.type === 'string', - StringFragment.from - ); - } - - static from(frag: StringFragmentInterface): StringFragment { - return new StringFragment(frag.value); - } - - readonly type: FragmentType = 'string'; - readonly isComplete = true; - constructor(readonly value: string) {} -} - -export class BooleanFragment { - static readonly schema = BooleanFragmentSchema; - - static as(data: unknown): BooleanFragment { - return as( - data, - 'boolean', - BooleanFragment, - (v: ValueFragmentInterface): v is BooleanFragmentInterface => - v.type === 'boolean', - BooleanFragment.from - ); - } - - static from(frag: BooleanFragmentInterface): BooleanFragment { - return new BooleanFragment(frag.value); - } - - readonly type: FragmentType = 'boolean'; - readonly isComplete = true; - readonly value: boolean; - constructor(input: boolean | string) { - this.value = typeof input === 'string' ? starlark.asBoolean(input) : input; - } +const AllFragmentsSchema = z.discriminatedUnion('type', [ + ArrayFragmentSchema, + AttributeFragmentSchema, + BooleanFragmentSchema, + RecordFragmentSchema, + StringFragmentSchema, +]); + +export type AllFragments = z.infer; +export type ArrayFragment = z.infer; +export type AttributeFragment = z.infer; +export type BooleanFragment = z.infer; +export type ChildFragments = Record; +export type PrimitiveFragments = z.infer; +export type RecordFragment = z.infer; +export type StringFragment = z.infer; +export type ValueFragments = z.infer; + +export function string(value: string): StringFragment { + return { + type: 'string', + isComplete: true, + value, + }; } -export class ArrayFragment { - static readonly schema = ArrayFragmentSchema; - - static as(data: unknown): ArrayFragment { - return as( - data, - 'array', - ArrayFragment, - (v: ValueFragmentInterface): v is ArrayFragmentInterface => - v.type === 'array', - ArrayFragment.from - ); - } - - static from(frag: ArrayFragmentInterface): ArrayFragment { - return new ArrayFragment( - frag.items.map((item) => asValue(item)), - frag.isComplete - ); - } - - readonly type: FragmentType = 'array'; - isComplete = false; - items: ValueFragment[] = []; - - constructor(items: ValueFragment[] = [], isComplete = false) { - this.items = items; - this.isComplete = isComplete; - } - - addValue(item: ValueFragment): void { - this.items.push(item); - } +export function boolean(value: string | boolean): BooleanFragment { + return { + type: 'boolean', + isComplete: true, + value: typeof value === 'string' ? starlark.asBoolean(value) : value, + }; } -export class RecordFragment { - static readonly schema = RecordFragmentSchema; - - static as(data: unknown): RecordFragment { - return as( - data, - 'record', - RecordFragment, - (v: ValueFragmentInterface): v is RecordFragmentInterface => - v.type === 'record', - RecordFragment.from - ); - } - - static from(frag: RecordFragmentInterface): RecordFragment { - const children: ChildFragments = Object.entries(frag.children).reduce( - (acc, propVal) => { - acc[propVal[0]] = asValue(propVal[1]); - return acc; - }, - {} as ChildFragments - ); - return new RecordFragment(children, frag.isComplete); - } - - static safeAs(data: unknown): RecordFragment | null { - try { - return RecordFragment.as(data); - } catch (e) { - return null; - } - } - - readonly type: FragmentType = 'record'; - isComplete: boolean; - children: ChildFragments; - - constructor(children: ChildFragments = {}, isComplete = false) { - this.children = children; - this.isComplete = isComplete; - } - - addAttribute(attrib: AttributeFragment): void { - if (!attrib.value) { - throw new Error('The attribute fragment does not have a value.'); - } - this.children[attrib.name] = attrib.value; - } +export function record( + children: ChildFragments = {}, + isComplete = false +): RecordFragment { + return { + type: 'record', + isComplete, + children, + }; } -export class AttributeFragment { - static readonly schema = AttributeFragmentSchema; - - static as(data: unknown): AttributeFragment { - if (data instanceof AttributeFragment) { - return data; - } - const frag = AttributeFragmentSchema.parse(data); - return new AttributeFragment( - frag.name, - frag.value ? asValue(frag.value) : undefined - ); - } - - static safeAs(data: unknown): AttributeFragment | null { - try { - return AttributeFragment.as(data); - } catch (e) { - return null; - } - } - - readonly type: FragmentType = 'attribute'; - readonly name: string; - value?: ValueFragment; - - constructor(name: string, value?: ValueFragment) { - this.name = name; - this.value = value; - } - - get isComplete(): boolean { - return is.truthy(this.value); - } - - addValue(item: ValueFragment): void { - this.value = item; - } -} - -export type ValueFragment = - | ArrayFragment - | BooleanFragment - | RecordFragment - | StringFragment; -export type ChildFragments = Record; -export type Fragment = ValueFragment | AttributeFragment; - -// Static Functions - -function as( - data: unknown, - expected: string, - fragCtor: new (...args: any[]) => F, - iTypeGuard: (v: ValueFragmentInterface) => v is I, - fromFn: (frag: I) => F -): F { - if (data instanceof fragCtor) { - return data; - } - const fragInterface = ValueFragmentSchema.parse(data); - if (iTypeGuard(fragInterface)) { - return fromFn(fragInterface); - } - throw new Error( - 'The data is a value fragment, but it is not the expected type.' + - `expected: ${expected}, actual: ${fragInterface.type}.` - ); +export function attribute( + name: string, + value?: ValueFragments, + isComplete = false +): AttributeFragment { + return { + type: 'attribute', + name, + value, + isComplete, + }; } -export function asValue(data: unknown): ValueFragment { - const frag = ValueFragmentSchema.parse(data); - switch (frag.type) { - case 'string': - return StringFragment.from(frag); - case 'boolean': - return BooleanFragment.from(frag); - case 'array': - return ArrayFragment.from(frag); - case 'record': - return RecordFragment.from(frag); - } - // istanbul ignore next: catch new fragment type - throw new Error('Unexpected fragment type.'); +export function array( + items: PrimitiveFragments[] = [], + isComplete = false +): ArrayFragment { + return { + type: 'array', + items, + isComplete, + }; } -export function safeAsValue(data: unknown): ValueFragment | null { - try { - return asValue(data); - } catch (e) { - return null; - } +export function isValue(data: unknown): data is ValueFragments { + const result = ValueFragmentsSchema.safeParse(data); + return result.success; } -export function asFragment(data: unknown): Fragment { - const value = safeAsValue(data); - if (value) { - return value; - } - return AttributeFragment.as(data); +export function isPrimitive(data: unknown): data is PrimitiveFragments { + const result = PrimitiveFragmentsSchema.safeParse(data); + return result.success; } diff --git a/lib/modules/manager/bazel-module/parser.spec.ts b/lib/modules/manager/bazel-module/parser.spec.ts index d307eb90174c12..56392fef8a50ec 100644 --- a/lib/modules/manager/bazel-module/parser.spec.ts +++ b/lib/modules/manager/bazel-module/parser.spec.ts @@ -1,5 +1,5 @@ import { codeBlock } from 'common-tags'; -import { BooleanFragment, RecordFragment, StringFragment } from './fragments'; +import * as fragments from './fragments'; import { parse } from './parser'; describe('modules/manager/bazel-module/parser', () => { @@ -21,20 +21,20 @@ describe('modules/manager/bazel-module/parser', () => { `; const res = parse(input); expect(res).toEqual([ - new RecordFragment( + fragments.record( { - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_foo'), - version: new StringFragment('1.2.3'), + rule: fragments.string('bazel_dep'), + name: fragments.string('rules_foo'), + version: fragments.string('1.2.3'), }, true ), - new RecordFragment( + fragments.record( { - rule: new StringFragment('bazel_dep'), - name: new StringFragment('rules_bar'), - version: new StringFragment('1.0.0'), - dev_dependency: new BooleanFragment(true), + rule: fragments.string('bazel_dep'), + name: fragments.string('rules_bar'), + version: fragments.string('1.0.0'), + dev_dependency: fragments.boolean(true), }, true ), diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index a31866383d4f37..2ce0a621f41e27 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -1,7 +1,7 @@ import { lang, query as q } from 'good-enough-parser'; import { regEx } from '../../../util/regex'; import { Ctx } from './context'; -import type { ValueFragment } from './fragments'; +import type { RecordFragment } from './fragments'; import * as starlark from './starlark'; const booleanValuesRegex = regEx(`^${starlark.booleanStringValues.join('|')}$`); @@ -52,7 +52,7 @@ const query = q.tree({ const starlarkLang = lang.createLang('starlark'); -export function parse(input: string): ValueFragment[] { +export function parse(input: string): RecordFragment[] { const parsedResult = starlarkLang.query(input, query, new Ctx()); if (parsedResult) { return parsedResult.results; From 416178bb4955f6b3677b2fa07cbc8c9d8fdc3b0b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 15:58:17 -0600 Subject: [PATCH 68/80] Refactor context processing to remove istanbul comment --- lib/modules/manager/bazel-module/context.ts | 29 ++++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 72b5d2c3c15816..94deaa3faa7716 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -17,7 +17,10 @@ export class CtxProcessingError extends Error { readonly current: AllFragments; readonly parent?: AllFragments; constructor(current: AllFragments, parent?: AllFragments) { - super(); + const msg = `Invalid context state. current: ${current.type}, parent: ${ + parent?.type ?? 'none' + }`; + super(msg); this.name = 'CtxProcessingError'; this.current = current; this.parent = parent; @@ -59,12 +62,14 @@ export class Ctx implements CtxCompatible { throw new Error('Requested current array, but does not exist.'); } - private popStack(): void { + private popStack(): boolean { const current = this.stack.pop(); - // TODO: Try to remove this istanbul comment. - // istanbul ignore if: we should never get here if (!current) { - return; + return false; + } + if (!current.isComplete) { + this.stack.push(current); + return false; } const parent = this.stack.safeCurrent; @@ -72,11 +77,11 @@ export class Ctx implements CtxCompatible { if (parent.type === 'attribute' && fragments.isValue(current)) { parent.value = current; parent.isComplete = true; - return; + return true; } if (parent.type === 'array' && fragments.isPrimitive(current)) { parent.items.push(current); - return; + return true; } if ( parent.type === 'record' && @@ -84,21 +89,19 @@ export class Ctx implements CtxCompatible { current.value !== undefined ) { parent.children[current.name] = current.value; - return; + return true; } } else if (current.type === 'record') { this.results.push(current); - return; + return true; } throw new CtxProcessingError(current, parent); } private processStack(): Ctx { - let current = this.stack.safeCurrent; - while (current?.isComplete) { - this.popStack(); - current = this.stack.safeCurrent; + while (this.popStack()) { + // Nothing to do } return this; } From bf8a43514137f71f0b4586715ae0193eb3109f69 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 16:17:35 -0600 Subject: [PATCH 69/80] Remove toPackageDependency --- .../manager/bazel-module/bazel-dep.spec.ts | 16 +++--- lib/modules/manager/bazel-module/bazel-dep.ts | 50 +++++++++++++------ lib/modules/manager/bazel-module/extract.ts | 20 +++----- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts index d4a521dc128a61..87c00a20b30149 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -1,9 +1,9 @@ import { BazelDatasource } from '../../datasource/bazel'; -import { toPackageDependency } from './bazel-dep'; +import { ToBazelDep } from './bazel-dep'; import * as fragments from './fragments'; describe('modules/manager/bazel-module/bazel-dep', () => { - describe('toPackageDependency()', () => { + describe('ToBazelDep', () => { it('transforms a record fragment', () => { const record = fragments.record({ rule: fragments.string('bazel_dep'), @@ -11,7 +11,7 @@ describe('modules/manager/bazel-module/bazel-dep', () => { version: fragments.string('1.2.3'), dev_dependency: fragments.boolean(true), }); - const result = toPackageDependency(record); + const result = ToBazelDep.parse(record); expect(result).toEqual({ datasource: BazelDatasource.id, depType: 'bazel_dep', @@ -20,10 +20,10 @@ describe('modules/manager/bazel-module/bazel-dep', () => { }); }); - it('returns null if invalid', () => { - const frag = fragments.record(); - const result = toPackageDependency(frag); - expect(result).toBeNull(); - }); + // it('returns null if invalid', () => { + // const frag = fragments.record(); + // const result = toPackageDependency(frag); + // expect(result).toBeNull(); + // }); }); }); diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index 3b5445761fc3e7..bb3ff04a7f562b 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -3,21 +3,41 @@ import { BazelDatasource } from '../../datasource/bazel'; import type { PackageDependency } from '../types'; import { BooleanFragmentSchema, - RecordFragment, + RecordFragmentSchema, StringFragmentSchema, } from './fragments'; -const BazelDepChildrenSchema = z.object({ - rule: StringFragmentSchema.extend({ - value: z.literal('bazel_dep'), +// const BazelDepChildrenSchema = z.object({ +// rule: StringFragmentSchema.extend({ +// value: z.literal('bazel_dep'), +// }), +// name: StringFragmentSchema, +// version: StringFragmentSchema, +// dev_dependency: BooleanFragmentSchema.optional(), +// }); +// +// export const ToBazelDep = BazelDepChildrenSchema.transform( +// ({ rule, name, version }): PackageDependency => ({ +// datasource: BazelDatasource.id, +// depType: rule.value, +// depName: name.value, +// currentValue: version.value, +// }) +// ); + +const BazelDepSchema = RecordFragmentSchema.extend({ + children: z.object({ + rule: StringFragmentSchema.extend({ + value: z.literal('bazel_dep'), + }), + name: StringFragmentSchema, + version: StringFragmentSchema, + dev_dependency: BooleanFragmentSchema.optional(), }), - name: StringFragmentSchema, - version: StringFragmentSchema, - dev_dependency: BooleanFragmentSchema.optional(), }); -const ToBazelDep = BazelDepChildrenSchema.transform( - ({ rule, name, version }): PackageDependency => ({ +export const ToBazelDep = BazelDepSchema.transform( + ({ children: { rule, name, version } }): PackageDependency => ({ datasource: BazelDatasource.id, depType: rule.value, depName: name.value, @@ -25,9 +45,9 @@ const ToBazelDep = BazelDepChildrenSchema.transform( }) ); -export function toPackageDependency( - record: RecordFragment -): PackageDependency | null { - const parseResult = ToBazelDep.safeParse(record.children); - return parseResult.success ? parseResult.data : null; -} +// export function toPackageDependency( +// record: RecordFragment +// ): PackageDependency | null { +// const parseResult = ToBazelDep.safeParse(record.children); +// return parseResult.success ? parseResult.data : null; +// } diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 351aca2bc640e5..ac393a4e65cbd1 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,22 +1,14 @@ -import { logger } from '../../../logger'; -import { isNotNullOrUndefined } from '../../../util/array'; +import { LooseArray } from '../../../util/schema-utils'; import type { PackageFileContent } from '../types'; -import { toPackageDependency } from './bazel-dep'; +import { ToBazelDep } from './bazel-dep'; import { parse } from './parser'; export function extractPackageFile( content: string, packageFile: string ): PackageFileContent | null { - const deps = parse(content) - .map((frag) => toPackageDependency(frag)) - .filter(isNotNullOrUndefined); - if (deps.length) { - return { deps }; - } - logger.debug( - { packageFile }, - 'The package file did not contain any package dependencies.' - ); - return null; + const records = parse(content); + return LooseArray(ToBazelDep) + .transform((deps) => (deps.length ? { deps } : null)) + .parse(records); } From 4aedab1ee8545b60402d949fff9c64a59cb2ef78 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Fri, 12 May 2023 16:19:43 -0600 Subject: [PATCH 70/80] Clean up --- .../manager/bazel-module/bazel-dep.spec.ts | 6 ----- lib/modules/manager/bazel-module/bazel-dep.ts | 25 ------------------- 2 files changed, 31 deletions(-) diff --git a/lib/modules/manager/bazel-module/bazel-dep.spec.ts b/lib/modules/manager/bazel-module/bazel-dep.spec.ts index 87c00a20b30149..2b4c0226219102 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.spec.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.spec.ts @@ -19,11 +19,5 @@ describe('modules/manager/bazel-module/bazel-dep', () => { currentValue: '1.2.3', }); }); - - // it('returns null if invalid', () => { - // const frag = fragments.record(); - // const result = toPackageDependency(frag); - // expect(result).toBeNull(); - // }); }); }); diff --git a/lib/modules/manager/bazel-module/bazel-dep.ts b/lib/modules/manager/bazel-module/bazel-dep.ts index bb3ff04a7f562b..8d6f6c85e6cb14 100644 --- a/lib/modules/manager/bazel-module/bazel-dep.ts +++ b/lib/modules/manager/bazel-module/bazel-dep.ts @@ -7,24 +7,6 @@ import { StringFragmentSchema, } from './fragments'; -// const BazelDepChildrenSchema = z.object({ -// rule: StringFragmentSchema.extend({ -// value: z.literal('bazel_dep'), -// }), -// name: StringFragmentSchema, -// version: StringFragmentSchema, -// dev_dependency: BooleanFragmentSchema.optional(), -// }); -// -// export const ToBazelDep = BazelDepChildrenSchema.transform( -// ({ rule, name, version }): PackageDependency => ({ -// datasource: BazelDatasource.id, -// depType: rule.value, -// depName: name.value, -// currentValue: version.value, -// }) -// ); - const BazelDepSchema = RecordFragmentSchema.extend({ children: z.object({ rule: StringFragmentSchema.extend({ @@ -44,10 +26,3 @@ export const ToBazelDep = BazelDepSchema.transform( currentValue: version.value, }) ); - -// export function toPackageDependency( -// record: RecordFragment -// ): PackageDependency | null { -// const parseResult = ToBazelDep.safeParse(record.children); -// return parseResult.success ? parseResult.data : null; -// } From 7a3896837a565424aa0b8e5b1ba2cf57b0e1cc9b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 10:39:07 -0600 Subject: [PATCH 71/80] Remove Stack --- lib/modules/manager/bazel-module/context.ts | 32 ++++++++++++------- .../manager/bazel-module/stack.spec.ts | 21 ------------ lib/modules/manager/bazel-module/stack.ts | 22 ------------- 3 files changed, 21 insertions(+), 54 deletions(-) delete mode 100644 lib/modules/manager/bazel-module/stack.spec.ts delete mode 100644 lib/modules/manager/bazel-module/stack.ts diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 94deaa3faa7716..49d17c9233a0d9 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -5,12 +5,11 @@ import type { RecordFragment, } from './fragments'; import * as fragments from './fragments'; -import { Stack } from './stack'; // Represents the fields that the context must have. export interface CtxCompatible { results: RecordFragment[]; - stack: Stack; + stack: AllFragments[]; } export class CtxProcessingError extends Error { @@ -29,12 +28,9 @@ export class CtxProcessingError extends Error { export class Ctx implements CtxCompatible { results: RecordFragment[]; - stack: Stack; + stack: AllFragments[]; - constructor( - results: RecordFragment[] = [], - stack = Stack.create() - ) { + constructor(results: RecordFragment[] = [], stack: AllFragments[] = []) { this.results = results; this.stack = stack; } @@ -43,11 +39,25 @@ export class Ctx implements CtxCompatible { if (obj instanceof Ctx) { return obj; } - return new Ctx(obj.results, Stack.create(...obj.stack)); + return new Ctx(obj.results, Array.from(obj.stack)); } + get safeCurrent(): AllFragments | undefined { + if (!this.stack.length) { + return undefined; + } + return this.stack[this.stack.length - 1]; + } + + get current(): AllFragments { + const c = this.safeCurrent; + if (c === undefined) { + throw new Error('Requested current, but no value.'); + } + return c; + } get currentRecord(): RecordFragment { - const current = this.stack.current; + const current = this.current; if (current.type === 'record') { return current; } @@ -55,7 +65,7 @@ export class Ctx implements CtxCompatible { } get currentArray(): ArrayFragment { - const current = this.stack.current; + const current = this.current; if (current.type === 'array') { return current; } @@ -71,7 +81,7 @@ export class Ctx implements CtxCompatible { this.stack.push(current); return false; } - const parent = this.stack.safeCurrent; + const parent = this.safeCurrent; if (parent) { if (parent.type === 'attribute' && fragments.isValue(current)) { diff --git a/lib/modules/manager/bazel-module/stack.spec.ts b/lib/modules/manager/bazel-module/stack.spec.ts deleted file mode 100644 index 81e55716e0e1ed..00000000000000 --- a/lib/modules/manager/bazel-module/stack.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Stack } from './stack'; - -describe('modules/manager/bazel-module/stack', () => { - describe('Stack', () => { - it.each` - items | exp - ${[]} | ${undefined} - ${['first', 'second']} | ${'second'} - `('safely get current for $items', ({ items, exp }) => { - const stack = Stack.create(...items); - expect(stack.safeCurrent).toBe(exp); - }); - - it('current throws if no items', () => { - const stack = Stack.create(); - expect(() => stack.current).toThrow( - new Error('Requested current, but no value.') - ); - }); - }); -}); diff --git a/lib/modules/manager/bazel-module/stack.ts b/lib/modules/manager/bazel-module/stack.ts deleted file mode 100644 index d9c5ceeee884b5..00000000000000 --- a/lib/modules/manager/bazel-module/stack.ts +++ /dev/null @@ -1,22 +0,0 @@ -export class Stack extends Array { - static create(...items: Array): Stack { - const stack = new Stack(); - stack.push(...items); - return stack; - } - - get safeCurrent(): T | undefined { - if (!this.length) { - return undefined; - } - return this[this.length - 1]; - } - - get current(): T { - const c = this.safeCurrent; - if (c === undefined) { - throw new Error('Requested current, but no value.'); - } - return c; - } -} From d6f85c2b7afbde8ee80b754d1b546c3b4f6b85a9 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 10:45:09 -0600 Subject: [PATCH 72/80] Add try-catch in extractPackageFile --- lib/modules/manager/bazel-module/extract.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index ac393a4e65cbd1..de7997e6b51ae0 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -1,3 +1,4 @@ +import { logger } from '../../../logger'; import { LooseArray } from '../../../util/schema-utils'; import type { PackageFileContent } from '../types'; import { ToBazelDep } from './bazel-dep'; @@ -7,8 +8,13 @@ export function extractPackageFile( content: string, packageFile: string ): PackageFileContent | null { - const records = parse(content); - return LooseArray(ToBazelDep) - .transform((deps) => (deps.length ? { deps } : null)) - .parse(records); + try { + const records = parse(content); + return LooseArray(ToBazelDep) + .transform((deps) => (deps.length ? { deps } : null)) + .parse(records); + } catch (err) { + logger.error({ err }, 'BazelModule'); + return null; + } } From c4e2d06713ef2910b60b7713f8dd30dfda52b97b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 10:51:47 -0600 Subject: [PATCH 73/80] Add tests for Ctx.current --- .../manager/bazel-module/context.spec.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index ec33564f5180d1..26b3410163baad 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -51,7 +51,22 @@ describe('modules/manager/bazel-module/context', () => { ]); }); - describe('currentRecord', () => { + describe('.current', () => { + it('returns the last fragment in the stack if it exists', () => { + const ctx = new Ctx().startRecord(); + const current = ctx.current; + expect(current.type).toBe('record'); + }); + + it('throws if the stack is empty', () => { + const ctx = new Ctx(); + expect(() => ctx.current).toThrow( + new Error('Requested current, but no value.') + ); + }); + }); + + describe('.currentRecord', () => { it('returns the record fragment if it is current', () => { const ctx = new Ctx().startRecord(); expect(ctx.currentRecord).toEqual(fragments.record()); @@ -65,7 +80,7 @@ describe('modules/manager/bazel-module/context', () => { }); }); - describe('currentArray', () => { + describe('.currentArray', () => { it('returns the array fragment if it is current', () => { const ctx = new Ctx().startArray(); expect(ctx.currentArray).toEqual(fragments.array()); From 951b14dc5bafaaa651a22210ded0f70d6cc454a2 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 11:00:44 -0600 Subject: [PATCH 74/80] Add test for try-catch --- lib/modules/manager/bazel-module/extract.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/modules/manager/bazel-module/extract.spec.ts b/lib/modules/manager/bazel-module/extract.spec.ts index 6182e57424b481..8207d4e0f5dcf8 100644 --- a/lib/modules/manager/bazel-module/extract.spec.ts +++ b/lib/modules/manager/bazel-module/extract.spec.ts @@ -1,5 +1,6 @@ import { codeBlock } from 'common-tags'; import { BazelDatasource } from '../../datasource/bazel'; +import * as parser from './parser'; import { extractPackageFile } from '.'; describe('modules/manager/bazel-module/extract', () => { @@ -9,6 +10,14 @@ describe('modules/manager/bazel-module/extract', () => { expect(result).toBeNull(); }); + it('returns null if something throws an error', () => { + jest.spyOn(parser, 'parse').mockImplementationOnce((input) => { + throw new Error('Test error'); + }); + const result = extractPackageFile('content', 'MODULE.bazel'); + expect(result).toBeNull(); + }); + it('returns null if file is empty', () => { const result = extractPackageFile('', 'MODULE.bazel'); expect(result).toBeNull(); From 9908d3e75b3613cdbf0084c666150111a831168d Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 12:41:49 -0600 Subject: [PATCH 75/80] Hide current and safeCurrent --- .../manager/bazel-module/context.spec.ts | 18 +++++------------- lib/modules/manager/bazel-module/context.ts | 9 +++------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index 26b3410163baad..650b2e7ba271b8 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -51,26 +51,18 @@ describe('modules/manager/bazel-module/context', () => { ]); }); - describe('.current', () => { - it('returns the last fragment in the stack if it exists', () => { + describe('.currentRecord', () => { + it('returns the record fragment if it is current', () => { const ctx = new Ctx().startRecord(); - const current = ctx.current; - expect(current.type).toBe('record'); + expect(ctx.currentRecord).toEqual(fragments.record()); }); - it('throws if the stack is empty', () => { + it('throws if there is no current', () => { const ctx = new Ctx(); - expect(() => ctx.current).toThrow( + expect(() => ctx.currentRecord).toThrow( new Error('Requested current, but no value.') ); }); - }); - - describe('.currentRecord', () => { - it('returns the record fragment if it is current', () => { - const ctx = new Ctx().startRecord(); - expect(ctx.currentRecord).toEqual(fragments.record()); - }); it('throws if the current is not a record fragment', () => { const ctx = new Ctx().startArray(); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 49d17c9233a0d9..294149b6bfaa4b 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -42,14 +42,11 @@ export class Ctx implements CtxCompatible { return new Ctx(obj.results, Array.from(obj.stack)); } - get safeCurrent(): AllFragments | undefined { - if (!this.stack.length) { - return undefined; - } - return this.stack[this.stack.length - 1]; + private get safeCurrent(): AllFragments | undefined { + return this.stack.at(-1); } - get current(): AllFragments { + private get current(): AllFragments { const c = this.safeCurrent; if (c === undefined) { throw new Error('Requested current, but no value.'); From f37aa4d8b15b54d479a22b29c32570fbfca1cf22 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 12:44:10 -0600 Subject: [PATCH 76/80] Rename packageFile to filename --- lib/modules/manager/bazel-module/extract.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index de7997e6b51ae0..4a8b26898af89d 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -6,7 +6,7 @@ import { parse } from './parser'; export function extractPackageFile( content: string, - packageFile: string + filename: string ): PackageFileContent | null { try { const records = parse(content); @@ -14,7 +14,7 @@ export function extractPackageFile( .transform((deps) => (deps.length ? { deps } : null)) .parse(records); } catch (err) { - logger.error({ err }, 'BazelModule'); + logger.error({ err, filename }, 'Failed to parse bazel module file.'); return null; } } From 36ba437e603323345ea04b94d444beb89ee847bb Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 12:47:46 -0600 Subject: [PATCH 77/80] Change to debug log --- lib/modules/manager/bazel-module/extract.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manager/bazel-module/extract.ts b/lib/modules/manager/bazel-module/extract.ts index 4a8b26898af89d..056900579d401a 100644 --- a/lib/modules/manager/bazel-module/extract.ts +++ b/lib/modules/manager/bazel-module/extract.ts @@ -14,7 +14,7 @@ export function extractPackageFile( .transform((deps) => (deps.length ? { deps } : null)) .parse(records); } catch (err) { - logger.error({ err, filename }, 'Failed to parse bazel module file.'); + logger.debug({ err, filename }, 'Failed to parse bazel module file.'); return null; } } From 738f769b66ce67551d30544272d3ebea62aaf79b Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Mon, 15 May 2023 16:22:08 -0600 Subject: [PATCH 78/80] Remove Ctx.as --- lib/modules/manager/bazel-module/context.spec.ts | 16 ---------------- lib/modules/manager/bazel-module/context.ts | 7 ------- lib/modules/manager/bazel-module/parser.ts | 10 +++++----- 3 files changed, 5 insertions(+), 28 deletions(-) diff --git a/lib/modules/manager/bazel-module/context.spec.ts b/lib/modules/manager/bazel-module/context.spec.ts index 650b2e7ba271b8..06d22d0b35884e 100644 --- a/lib/modules/manager/bazel-module/context.spec.ts +++ b/lib/modules/manager/bazel-module/context.spec.ts @@ -95,20 +95,4 @@ describe('modules/manager/bazel-module/context', () => { ); }); }); - - describe('Ctx.as', () => { - it('adds the appropriate prototype to the context and referenced fragments', () => { - // Ensure that we have values in results (bazel_dep) and the stack (foo_library). - const ctx = new Ctx() - .startRule('bazel_dep') - .startAttribute('name') - .addString('rules_foo') - .startAttribute('version') - .addString('1.2.3') - .endRule() - .startRule('foo_library'); - const result = Ctx.as(ctx); - expect(result).toEqual(ctx); - }); - }); }); diff --git a/lib/modules/manager/bazel-module/context.ts b/lib/modules/manager/bazel-module/context.ts index 294149b6bfaa4b..b590a560e02652 100644 --- a/lib/modules/manager/bazel-module/context.ts +++ b/lib/modules/manager/bazel-module/context.ts @@ -35,13 +35,6 @@ export class Ctx implements CtxCompatible { this.stack = stack; } - static as(obj: CtxCompatible): Ctx { - if (obj instanceof Ctx) { - return obj; - } - return new Ctx(obj.results, Array.from(obj.stack)); - } - private get safeCurrent(): AllFragments | undefined { return this.stack.at(-1); } diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 2ce0a621f41e27..4a5d3bc96c17ec 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -15,21 +15,21 @@ const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); **/ const kvParams = q .sym((ctx, token) => { - return Ctx.as(ctx).startAttribute(token.value); + return ctx.startAttribute(token.value); }) .op('=') .alt( q.str((ctx, token) => { - return Ctx.as(ctx).addString(token.value); + return ctx.addString(token.value); }), q.sym(booleanValuesRegex, (ctx, token) => { - return Ctx.as(ctx).addBoolean(token.value); + return ctx.addBoolean(token.value); }) ); const moduleRules = q .sym(supportedRulesRegex, (ctx, token) => { - return Ctx.as(ctx).startRule(token.value); + return ctx.startRule(token.value); }) .join( q.tree({ @@ -37,7 +37,7 @@ const moduleRules = q maxDepth: 1, search: kvParams, postHandler: (ctx, tree) => { - return Ctx.as(ctx).endRule(); + return ctx.endRule(); }, }) ); From f29f956bc969fa30161acdccb09afef85a675f36 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 18 May 2023 08:14:16 -0600 Subject: [PATCH 79/80] Use LooseArray and LooseRecord --- lib/modules/manager/bazel-module/fragments.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/modules/manager/bazel-module/fragments.ts b/lib/modules/manager/bazel-module/fragments.ts index 5237cb06ec7600..0a873a4b2eed4a 100644 --- a/lib/modules/manager/bazel-module/fragments.ts +++ b/lib/modules/manager/bazel-module/fragments.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { LooseArray, LooseRecord } from '../../../util/schema-utils'; import * as starlark from './starlark'; export const StringFragmentSchema = z.object({ @@ -17,7 +18,7 @@ const PrimitiveFragmentsSchema = z.discriminatedUnion('type', [ ]); export const ArrayFragmentSchema = z.object({ type: z.literal('array'), - items: PrimitiveFragmentsSchema.array(), + items: LooseArray(PrimitiveFragmentsSchema), isComplete: z.boolean(), }); const ValueFragmentsSchema = z.discriminatedUnion('type', [ @@ -27,7 +28,7 @@ const ValueFragmentsSchema = z.discriminatedUnion('type', [ ]); export const RecordFragmentSchema = z.object({ type: z.literal('record'), - children: z.record(z.string(), ValueFragmentsSchema), + children: LooseRecord(ValueFragmentsSchema), isComplete: z.boolean(), }); export const AttributeFragmentSchema = z.object({ From 100ccbfd9d3eb716a8654ad976a9d28943d7cfb7 Mon Sep 17 00:00:00 2001 From: Chuck Grindel Date: Thu, 18 May 2023 08:19:03 -0600 Subject: [PATCH 80/80] Use arrow functions in parser and use conditional return --- lib/modules/manager/bazel-module/parser.ts | 25 ++++++---------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/modules/manager/bazel-module/parser.ts b/lib/modules/manager/bazel-module/parser.ts index 4a5d3bc96c17ec..6a33772183e942 100644 --- a/lib/modules/manager/bazel-module/parser.ts +++ b/lib/modules/manager/bazel-module/parser.ts @@ -14,31 +14,21 @@ const supportedRulesRegex = regEx(`^${supportedRules.join('|')}$`); * - `dev_dependeny = True` **/ const kvParams = q - .sym((ctx, token) => { - return ctx.startAttribute(token.value); - }) + .sym((ctx, token) => ctx.startAttribute(token.value)) .op('=') .alt( - q.str((ctx, token) => { - return ctx.addString(token.value); - }), - q.sym(booleanValuesRegex, (ctx, token) => { - return ctx.addBoolean(token.value); - }) + q.str((ctx, token) => ctx.addString(token.value)), + q.sym(booleanValuesRegex, (ctx, token) => ctx.addBoolean(token.value)) ); const moduleRules = q - .sym(supportedRulesRegex, (ctx, token) => { - return ctx.startRule(token.value); - }) + .sym(supportedRulesRegex, (ctx, token) => ctx.startRule(token.value)) .join( q.tree({ type: 'wrapped-tree', maxDepth: 1, search: kvParams, - postHandler: (ctx, tree) => { - return ctx.endRule(); - }, + postHandler: (ctx, tree) => ctx.endRule(), }) ); @@ -54,8 +44,5 @@ const starlarkLang = lang.createLang('starlark'); export function parse(input: string): RecordFragment[] { const parsedResult = starlarkLang.query(input, query, new Ctx()); - if (parsedResult) { - return parsedResult.results; - } - return []; + return parsedResult?.results ?? []; }