import { query as q } from '@renovatebot/good-enough-parser'; import { regEx } from '../../../util/regex.ts'; import { CpanDatasource } from '../../datasource/cpan/index.ts'; import { GithubTagsDatasource } from '../../datasource/github-tags/index.ts'; import * as perlVersioning from '../../versioning/perl/index.ts'; import type { PackageDependency } from '../types.ts'; import { cpanfile } from './language.ts'; interface Ctx { deps: PackageDependency[]; perlVersion?: string; phase?: string; tempPhase?: string; depName?: string; currentValue?: string; } // requires perl, '5.36.1'; // requires 'perl' => 5.036001; const perlVersionMatch = q .sym('requires') .alt(q.sym('perl'), q.str('perl')) .alt(q.op(','), q.op('=>')) .alt( q.num((ctx, { value: perlVersion }) => ({ ...ctx, perlVersion })), q.str((ctx, { value: perlVersion }) => ({ ...ctx, perlVersion })), ) .op(';') .handler((ctx) => { if (ctx.perlVersion) { ctx.deps.push({ depName: 'perl', packageName: 'Perl/perl5', currentValue: ctx.perlVersion, datasource: GithubTagsDatasource.id, versioning: perlVersioning.id, extractVersion: '^v(?\\S+)', }); } return ctx; }); const requirementMatch = q.sym( regEx(/^(?:requires|recommends|suggests)$/), ); const phasedRequiresMatch = q.sym( regEx(/^(?:configure|build|test|author)_requires$/), (ctx, { value: phase }) => { ctx.tempPhase = phase .replace(regEx(/_requires/), '') .replace(regEx(/author/), 'develop'); return ctx; }, ); // requires 'Foo::Bar'; // // requires 'Foo::Bar', '1.23'; // recommends 'Foo::Bar', '1.23'; // suggests 'Foo::Bar', '1.23'; // // configure_requires 'Foo::Bar' => 1.023; // build_requires 'Foo::Bar' => 1.023; // test_requires 'Foo::Bar' => 1.023; // author_requires 'Foo::Bar' => 1.023; const moduleMatch = q .alt(requirementMatch, phasedRequiresMatch) .str((ctx, { value: depName }) => ({ ...ctx, depName })) .opt( q.alt(q.op(','), q.op('=>')).alt( q.num((ctx, { value: currentValue }) => ({ ...ctx, currentValue })), q.str((ctx, { value }) => { const currentValue = value.replace( regEx(/^(?:\s*(?:==|>=|>))?\s*v?/), '', ); return { ...ctx, currentValue }; }), ), ) .op(';') .handler((ctx) => { const { phase, tempPhase, depName, currentValue } = ctx; delete ctx.tempPhase; delete ctx.depName; delete ctx.currentValue; if (depName) { const dep: PackageDependency = { depName, }; if (currentValue) { dep.currentValue = currentValue; } else { dep.skipReason = 'unspecified-version'; } if (phase) { dep.depType = phase; } else if (tempPhase) { dep.depType = tempPhase; } dep.datasource = CpanDatasource.id; ctx.deps.push(dep); } return ctx; }); const phaseRegex = regEx(/^(?:configure|build|test|runtime|develop)/); const phaseMatch = q.alt( q.sym(phaseRegex, (ctx, { value: phase }) => ({ ...ctx, phase })), q.str(phaseRegex, (ctx, { value: phase }) => ({ ...ctx, phase })), ); // on 'configure' => sub { // on build => sub { // on 'test' => sub { // on runtime => sub { // on 'develop' => sub { const onMatch = q .sym('on') .join(phaseMatch) .op('=>') .sym('sub') .tree({ type: 'wrapped-tree', maxDepth: 1, search: moduleMatch, }) .handler((ctx) => { delete ctx.phase; return ctx; }); const query = q.tree({ type: 'root-tree', maxDepth: 4, search: q.alt(perlVersionMatch, moduleMatch, onMatch), }); export function parse( content: string, ): Pick | null { return cpanfile.query(content, query, { deps: [], }); }