Skip to content

Commit

Permalink
feat(manager): add cpanfile manager (#21152)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikesyo committed May 15, 2023
1 parent b081307 commit 9f9c1d9
Show file tree
Hide file tree
Showing 7 changed files with 511 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Expand Up @@ -19,6 +19,7 @@ import * as cloudbuild from './cloudbuild';
import * as cocoapods from './cocoapods';
import * as composer from './composer';
import * as conan from './conan';
import * as cpanfile from './cpanfile';
import * as depsEdn from './deps-edn';
import * as dockerCompose from './docker-compose';
import * as dockerfile from './dockerfile';
Expand Down Expand Up @@ -108,6 +109,7 @@ api.set('cloudbuild', cloudbuild);
api.set('cocoapods', cocoapods);
api.set('composer', composer);
api.set('conan', conan);
api.set('cpanfile', cpanfile);
api.set('deps-edn', depsEdn);
api.set('docker-compose', dockerCompose);
api.set('dockerfile', dockerfile);
Expand Down
285 changes: 285 additions & 0 deletions lib/modules/manager/cpanfile/extract.spec.ts
@@ -0,0 +1,285 @@
import { codeBlock } from 'common-tags';
import { extractPackageFile } from './extract';

describe('modules/manager/cpanfile/extract', () => {
describe('extractPackageFile()', () => {
it('returns null for empty', () => {
expect(extractPackageFile('', 'cpanfile')).toBeNull();
expect(extractPackageFile('nothing here', 'cpanfile')).toBeNull();
});

describe('parse perl', () => {
test.each`
version | expected
${'5.012005'} | ${'5.012005'}
${`'5.008001'`} | ${'5.008001'}
${`"5.008001"`} | ${'5.008001'}
`('$version', ({ version, expected }) => {
expect(
extractPackageFile(
`requires 'perl', ${version as string};`,
'cpanfile'
)
).toEqual({
deps: [
{
versioning: 'perl',
depName: 'perl',
packageName: 'Perl/perl5',
currentValue: expected,
datasource: 'github-tags',
extractVersion: '^v(?<version>\\S+)',
},
],
extractedConstraints: { perl: expected },
});
});
});

it('parse modules with requires', () => {
expect(
extractPackageFile(
codeBlock`
requires 'Try::Tiny';
requires 'URI', '1.59';
requires 'HTTP::Tiny', 0.034;
requires "Capture::Tiny" => "0";
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'Try::Tiny',
skipReason: 'no-version',
},
{
datasource: 'cpan',
depName: 'URI',
currentValue: '1.59',
},
{
datasource: 'cpan',
depName: 'HTTP::Tiny',
currentValue: '0.034',
},
{
datasource: 'cpan',
depName: 'Capture::Tiny',
currentValue: '0',
},
],
});
});

it('parse modules with recommends', () => {
expect(
extractPackageFile(
codeBlock`
recommends 'Crypt::URandom';
recommends 'HTTP::XSCookies', '0.000015';
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'Crypt::URandom',
skipReason: 'no-version',
},
{
datasource: 'cpan',
depName: 'HTTP::XSCookies',
currentValue: '0.000015',
},
],
});
});

it('parse modules with suggests', () => {
expect(
extractPackageFile(
codeBlock`
suggests 'Test::MockTime::HiRes', '0.06';
suggests 'Authen::Simple::Passwd';
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'Test::MockTime::HiRes',
currentValue: '0.06',
},
{
datasource: 'cpan',
depName: 'Authen::Simple::Passwd',
skipReason: 'no-version',
},
],
});
});

describe('parse modules with phases', () => {
test('configure phase', () => {
expect(
extractPackageFile(
codeBlock`
on 'configure' => sub {
requires "ExtUtils::MakeMaker" => "0";
};
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'ExtUtils::MakeMaker',
currentValue: '0',
depType: 'configure',
},
],
});
});

test('build phase', () => {
expect(
extractPackageFile(
codeBlock`
on build => sub {
requires 'Test::More', '0.98';
};
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'Test::More',
currentValue: '0.98',
depType: 'build',
},
],
});
});

test('test phase', () => {
expect(
extractPackageFile(
codeBlock`
on test => sub {
requires 'Test::More', '0.88';
requires 'Test::Requires';
};
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'Test::More',
currentValue: '0.88',
depType: 'test',
},
{
datasource: 'cpan',
depName: 'Test::Requires',
depType: 'test',
skipReason: 'no-version',
},
],
});
});

test('runtime phase', () => {
expect(
extractPackageFile(
codeBlock`
on runtime => sub {
suggests 'FCGI';
suggests 'FCGI::ProcManager';
};
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'FCGI',
depType: 'runtime',
skipReason: 'no-version',
},
{
datasource: 'cpan',
depName: 'FCGI::ProcManager',
depType: 'runtime',
skipReason: 'no-version',
},
],
});
});

test('develop phase', () => {
expect(
extractPackageFile(
codeBlock`
on 'develop' => sub {
requires "IPC::Open3" => "0";
requires "Term::Table" => "0.013";
};
`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'IPC::Open3',
currentValue: '0',
depType: 'develop',
},
{
datasource: 'cpan',
depName: 'Term::Table',
currentValue: '0.013',
depType: 'develop',
},
],
});
});
});

describe('parse modules with phase shortcuts', () => {
test.each`
shortcut | phase
${'configure_requires'} | ${'configure'}
${'build_requires'} | ${'build'}
${'test_requires'} | ${'test'}
${'author_requires'} | ${'develop'}
`('$shortcut', ({ shortcut, phase }) => {
expect(
extractPackageFile(
`${shortcut as string} 'Capture::Tiny', '0.12';`,
'cpanfile'
)
).toEqual({
deps: [
{
datasource: 'cpan',
depName: 'Capture::Tiny',
currentValue: '0.12',
depType: phase,
},
],
});
});
});
});
});
16 changes: 16 additions & 0 deletions lib/modules/manager/cpanfile/extract.ts
@@ -0,0 +1,16 @@
import type { PackageFileContent } from '../types';
import { parse } from './parser';

export function extractPackageFile(
content: string,
packageFile?: string
): PackageFileContent | null {
const result = parse(content);
if (!result?.deps.length) {
return null;
}

const { deps, perlVersion } = result;
const extractedConstraints = perlVersion ? { perl: perlVersion } : undefined;
return { deps, ...(extractedConstraints && { extractedConstraints }) };
}
17 changes: 17 additions & 0 deletions lib/modules/manager/cpanfile/index.ts
@@ -0,0 +1,17 @@
import { CpanDatasource } from '../../datasource/cpan';
import { GithubTagsDatasource } from '../../datasource/github-tags';

export { extractPackageFile } from './extract';

export const displayName = 'cpanfile';
export const url =
'https://metacpan.org/dist/Module-CPANfile/view/lib/cpanfile.pod';

export const defaultConfig = {
fileMatch: ['(^|/)cpanfile$'],
};

export const supportedDatasources = [
CpanDatasource.id,
GithubTagsDatasource.id,
];
44 changes: 44 additions & 0 deletions lib/modules/manager/cpanfile/language.ts
@@ -0,0 +1,44 @@
import { lexer as l, lang, parser as p } from 'good-enough-parser';

/**
* @see https://perldoc.perl.org/perldata#Scalar-value-constructors
*/
const bindigit = '[01]';
const octdigit = '[0-7]';
const digit = '[0-9]';
const nonzerodigit = '[1-9]';
const hexdigit = `(?:${digit}|[a-fA-F])`;

const bininteger = `(?:0[bB](?:_?${bindigit})+)`;
const octinteger = `(?:0(?:_?${octdigit})+)`;
const hexinteger = `(?:0[xX](?:_?${hexdigit})+)`;
const decinteger = `(?:${nonzerodigit}(?:_?${digit})*|0+(?:_?0)*)`;
const integer = `(?:${decinteger}|${bininteger}|${octinteger}|${hexinteger})`;

const digitpart = `(?:${digit}(?:_?${digit})*)`;
const fraction = `(?:\\.${digitpart})`;
const exponent = `(?:[eE][-+]?${digitpart})`;
const pointfloat = `(?:${digitpart}?${fraction}|${digitpart}\\.)`;
const exponentfloat = `(?:(?:${digitpart}|${pointfloat})${exponent})`;
const floatnumber = `(?:${pointfloat}|${exponentfloat})`;

const numbers = new RegExp(`(?:${floatnumber}|${integer})`);

const lexer: l.LexerConfig = {
joinLines: null,
comments: [{ type: 'line-comment', startsWith: '#' }],
symbols: /[_a-zA-Z][_a-zA-Z0-9]*/,
numbers,
operators: ['==', '>=', '>', '=>', ',', ';'],
brackets: [
{ startsWith: '{', endsWith: '}' },
{ startsWith: '(', endsWith: ')' },
],
strings: [{ startsWith: "'" }, { startsWith: '"' }],
};

const parser: p.ParserConfig = {
useIndentBlocks: false,
};

export const cpanfile = lang.createLang({ lexer, parser });

0 comments on commit 9f9c1d9

Please sign in to comment.