renovate/lib/modules/manager/bazel/parser.ts

329 lines
8.6 KiB
TypeScript

import type { lexer, parser } from 'good-enough-parser';
import { lang, query as q } from 'good-enough-parser';
import { logger } from '../../../logger';
import * as memCache from '../../../util/cache/memory';
import { hash } from '../../../util/hash';
import { supportedRulesRegex } from './rules';
import type { NestedFragment, RecordFragment } from './types';
interface Ctx {
readonly source: string;
results: RecordFragment[];
stack: NestedFragment[];
recordKey?: string;
subRecordKey?: string;
argIndex?: number;
}
function emptyCtx(source: string): Ctx {
return {
source,
results: [],
stack: [],
};
}
function currentFragment(ctx: Ctx): NestedFragment {
const deepestFragment = ctx.stack[ctx.stack.length - 1];
return deepestFragment;
}
function extractTreeValue(
source: string,
tree: parser.Tree,
offset: number,
): string {
if (tree.type === 'wrapped-tree') {
const { endsWith } = tree;
const to = endsWith.offset + endsWith.value.length;
return source.slice(offset, to);
}
// istanbul ignore next
return '';
}
/**
* 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>((ctx, { value: recordKey }) => {
return { ...ctx, recordKey };
})
.op('=')
.alt(
// string
q.str((ctx, { offset, value }) => {
const frag = currentFragment(ctx);
if (frag.type === 'record' && ctx.recordKey) {
const key = ctx.recordKey;
frag.children[key] = { type: 'string', value, offset };
}
return ctx;
}),
// array of strings or calls
q.tree({
type: 'wrapped-tree',
maxDepth: 1,
startsWith: '[',
endsWith: ']',
preHandler: (ctx, tree) => {
const parentRecord = currentFragment(ctx) as RecordFragment;
if (
parentRecord.type === 'record' &&
ctx.recordKey &&
tree.type === 'wrapped-tree'
) {
const key = ctx.recordKey;
parentRecord.children[key] = {
type: 'array',
value: '',
offset: tree.startsWith.offset,
children: [],
};
}
return ctx;
},
search: q.alt(
q.str<Ctx>((ctx, { value, offset }) => {
const parentRecord = currentFragment(ctx);
if (parentRecord.type === 'record' && ctx.recordKey) {
const key = ctx.recordKey;
const array = parentRecord.children[key];
if (array.type === 'array') {
array.children.push({ type: 'string', value, offset });
}
}
return ctx;
}),
q
.sym<Ctx>()
.handler(recordStartHandler)
.handler((ctx, { value, offset }) => {
const ruleFragment = currentFragment(ctx);
if (ruleFragment.type === 'record') {
ruleFragment.children._function = {
type: 'string',
value,
offset,
};
}
return ctx;
})
.many(
q.op<Ctx>('.').sym((ctx, { value }) => {
const ruleFragment = currentFragment(ctx);
if (
ruleFragment.type === 'record' &&
ruleFragment.children._function
) {
ruleFragment.children._function.value += `.${value}`;
}
return ctx;
}),
0,
3,
)
.tree({
type: 'wrapped-tree',
maxDepth: 1,
startsWith: '(',
endsWith: ')',
search: q
.opt(
q
.sym<Ctx>((ctx, { value: subRecordKey }) => ({
...ctx,
subRecordKey,
}))
.op('='),
)
.str((ctx, { value: subRecordValue, offset }) => {
const argIndex = ctx.argIndex ?? 0;
const subRecordKey = ctx.subRecordKey! ?? argIndex.toString();
const ruleFragment = currentFragment(ctx);
if (ruleFragment.type === 'record') {
ruleFragment.children[subRecordKey] = {
type: 'string',
value: subRecordValue,
offset,
};
}
delete ctx.subRecordKey;
ctx.argIndex = argIndex + 1;
return ctx;
}),
postHandler: (ctx, tree) => {
delete ctx.argIndex;
const callFrag = currentFragment(ctx);
ctx.stack.pop();
if (callFrag.type === 'record' && tree.type === 'wrapped-tree') {
callFrag.value = extractTreeValue(
ctx.source,
tree,
callFrag.offset,
);
const parentRecord = currentFragment(ctx);
if (parentRecord.type === 'record' && ctx.recordKey) {
const key = ctx.recordKey;
const array = parentRecord.children[key];
if (array.type === 'array') {
array.children.push(callFrag);
}
}
}
return ctx;
},
}),
),
postHandler: (ctx, tree) => {
const parentRecord = currentFragment(ctx);
if (
parentRecord.type === 'record' &&
ctx.recordKey &&
tree.type === 'wrapped-tree'
) {
const key = ctx.recordKey;
const array = parentRecord.children[key];
if (array.type === 'array') {
array.value = extractTreeValue(ctx.source, tree, array.offset);
}
}
return ctx;
},
}),
)
.handler((ctx) => {
delete ctx.recordKey;
return ctx;
});
/**
* Matches rule signature:
* `git_repository(......)`
* ^^^^^^^^
*
* @param search something to match inside parens
*/
function ruleCall(
search: q.QueryBuilder<Ctx, parser.Node>,
): q.QueryBuilder<Ctx, parser.Node> {
return q.tree({
type: 'wrapped-tree',
maxDepth: 1,
search,
postHandler: (ctx, tree) => {
const frag = currentFragment(ctx);
if (frag.type === 'record' && tree.type === 'wrapped-tree') {
frag.value = extractTreeValue(ctx.source, tree, frag.offset);
ctx.stack.pop();
ctx.results.push(frag);
}
return ctx;
},
});
}
function recordStartHandler(ctx: Ctx, { offset }: lexer.Token): Ctx {
ctx.stack.push({
type: 'record',
value: '',
offset,
children: {},
});
return ctx;
}
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;
}
/**
* Matches regular rules:
* - `git_repository(...)`
* - `_go_repository(...)`
*/
const regularRule = q
.sym<Ctx>(supportedRulesRegex, (ctx, token) =>
ruleNameHandler(recordStartHandler(ctx, token), token),
)
.join(ruleCall(kwParams));
/**
* Matches "maybe"-form rules:
* - `maybe(git_repository, ...)`
* - `maybe(_go_repository, ...)`
*/
const maybeRule = q
.sym<Ctx>('maybe', recordStartHandler)
.join(
ruleCall(
q.alt(
q.begin<Ctx>().sym(supportedRulesRegex, ruleNameHandler).op(','),
kwParams,
),
),
);
const rule = q.alt<Ctx>(maybeRule, regularRule);
const query = q.tree<Ctx>({
type: 'root-tree',
maxDepth: 16,
search: rule,
});
function getCacheKey(input: string): string {
const hashedInput = hash(input);
return `bazel-parser-${hashedInput}`;
}
const starlark = lang.createLang('starlark');
export function parse(
input: string,
packageFile?: string,
): RecordFragment[] | null {
const cacheKey = getCacheKey(input);
const cachedResult = memCache.get<RecordFragment[] | null>(cacheKey);
// istanbul ignore if
if (cachedResult === null || cachedResult) {
return cachedResult;
}
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');
}
memCache.set(cacheKey, result);
return result;
}