Skip to content

Commit

Permalink
feat: initial implementation of bazel-module manager (#21893)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgrindel committed May 20, 2023
1 parent 735129b commit 7a1d242
Show file tree
Hide file tree
Showing 15 changed files with 711 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -96,6 +97,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);
Expand Down
23 changes: 23 additions & 0 deletions lib/modules/manager/bazel-module/bazel-dep.spec.ts
@@ -0,0 +1,23 @@
import { BazelDatasource } from '../../datasource/bazel';
import { ToBazelDep } from './bazel-dep';
import * as fragments from './fragments';

describe('modules/manager/bazel-module/bazel-dep', () => {
describe('ToBazelDep', () => {
it('transforms a record fragment', () => {
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 = ToBazelDep.parse(record);
expect(result).toEqual({
datasource: BazelDatasource.id,
depType: 'bazel_dep',
depName: 'rules_foo',
currentValue: '1.2.3',
});
});
});
});
28 changes: 28 additions & 0 deletions lib/modules/manager/bazel-module/bazel-dep.ts
@@ -0,0 +1,28 @@
import { z } from 'zod';
import { BazelDatasource } from '../../datasource/bazel';
import type { PackageDependency } from '../types';
import {
BooleanFragmentSchema,
RecordFragmentSchema,
StringFragmentSchema,
} from './fragments';

const BazelDepSchema = RecordFragmentSchema.extend({
children: z.object({
rule: StringFragmentSchema.extend({
value: z.literal('bazel_dep'),
}),
name: StringFragmentSchema,
version: StringFragmentSchema,
dev_dependency: BooleanFragmentSchema.optional(),
}),
});

export const ToBazelDep = BazelDepSchema.transform(
({ children: { rule, name, version } }): PackageDependency => ({
datasource: BazelDatasource.id,
depType: rule.value,
depName: name.value,
currentValue: version.value,
})
);
98 changes: 98 additions & 0 deletions lib/modules/manager/bazel-module/context.spec.ts
@@ -0,0 +1,98 @@
import { Ctx, CtxProcessingError } from './context';
import * as fragments 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([
fragments.record(
{
rule: fragments.string('bazel_dep'),
name: fragments.string('rules_foo'),
version: fragments.string('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()
.addString('first')
.addString('second')
.endArray()
.endRule();

expect(ctx.results).toEqual([
fragments.record(
{
rule: fragments.string('foo_library'),
name: fragments.string('my_library'),
srcs: fragments.array(
[fragments.string('first'), fragments.string('second')],
true
),
},
true
),
]);
});

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 there is no current', () => {
const ctx = new Ctx();
expect(() => ctx.currentRecord).toThrow(
new Error('Requested current, but no value.')
);
});

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.')
);
});
});

describe('.currentArray', () => {
it('returns the array fragment if it is current', () => {
const ctx = new Ctx().startArray();
expect(ctx.currentArray).toEqual(fragments.array());
});

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.')
);
});
});

it('throws if add an attribute without a parent', () => {
const ctx = new Ctx().startAttribute('name');
expect(() => ctx.addString('chicken')).toThrow(
new CtxProcessingError(
fragments.attribute('name', fragments.string('chicken'))
)
);
});
});
});
154 changes: 154 additions & 0 deletions lib/modules/manager/bazel-module/context.ts
@@ -0,0 +1,154 @@
import type {
AllFragments,
ArrayFragment,
ChildFragments,
RecordFragment,
} from './fragments';
import * as fragments from './fragments';

// Represents the fields that the context must have.
export interface CtxCompatible {
results: RecordFragment[];
stack: AllFragments[];
}

export class CtxProcessingError extends Error {
readonly current: AllFragments;
readonly parent?: AllFragments;
constructor(current: AllFragments, parent?: AllFragments) {
const msg = `Invalid context state. current: ${current.type}, parent: ${
parent?.type ?? 'none'
}`;
super(msg);
this.name = 'CtxProcessingError';
this.current = current;
this.parent = parent;
}
}

export class Ctx implements CtxCompatible {
results: RecordFragment[];
stack: AllFragments[];

constructor(results: RecordFragment[] = [], stack: AllFragments[] = []) {
this.results = results;
this.stack = stack;
}

private get safeCurrent(): AllFragments | undefined {
return this.stack.at(-1);
}

private 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.current;
if (current.type === 'record') {
return current;
}
throw new Error('Requested current record, but does not exist.');
}

get currentArray(): ArrayFragment {
const current = this.current;
if (current.type === 'array') {
return current;
}
throw new Error('Requested current array, but does not exist.');
}

private popStack(): boolean {
const current = this.stack.pop();
if (!current) {
return false;
}
if (!current.isComplete) {
this.stack.push(current);
return false;
}
const parent = this.safeCurrent;

if (parent) {
if (parent.type === 'attribute' && fragments.isValue(current)) {
parent.value = current;
parent.isComplete = true;
return true;
}
if (parent.type === 'array' && fragments.isPrimitive(current)) {
parent.items.push(current);
return true;
}
if (
parent.type === 'record' &&
current.type === 'attribute' &&
current.value !== undefined
) {
parent.children[current.name] = current.value;
return true;
}
} else if (current.type === 'record') {
this.results.push(current);
return true;
}

throw new CtxProcessingError(current, parent);
}

private processStack(): Ctx {
while (this.popStack()) {
// Nothing to do
}
return this;
}

addString(value: string): Ctx {
this.stack.push(fragments.string(value));
return this.processStack();
}

addBoolean(value: string | boolean): Ctx {
this.stack.push(fragments.boolean(value));
return this.processStack();
}

startRecord(children: ChildFragments = {}): Ctx {
const record = fragments.record(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: fragments.string(name) });
}

endRule(): Ctx {
return this.endRecord();
}

startAttribute(name: string): Ctx {
this.stack.push(fragments.attribute(name));
return this.processStack();
}

startArray(): Ctx {
this.stack.push(fragments.array());
return this.processStack();
}

endArray(): Ctx {
const array = this.currentArray;
array.isComplete = true;
return this.processStack();
}
}
58 changes: 58 additions & 0 deletions lib/modules/manager/bazel-module/extract.spec.ts
@@ -0,0 +1,58 @@
import { codeBlock } from 'common-tags';
import { BazelDatasource } from '../../datasource/bazel';
import * as parser from './parser';
import { extractPackageFile } from '.';

describe('modules/manager/bazel-module/extract', () => {
describe('extractPackageFile()', () => {
it('returns null if fails to parse', () => {
const result = extractPackageFile('blahhhhh:foo:@what\n', 'MODULE.bazel');
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();
});

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")
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',
},
],
});
});
});
});

0 comments on commit 7a1d242

Please sign in to comment.