refactor(composer): Use schema for parsing ()

pull/22019/head
Sergei Zharinov 2023-05-07 23:51:20 +03:00 committed by GitHub
parent 546a52cbe6
commit 664dc808e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 589 additions and 387 deletions

View File

@ -1,5 +1,6 @@
import is from '@sindresorhus/is';
import { quote } from 'shlex';
import { z } from 'zod';
import {
SYSTEM_INSUFFICIENT_DISK_SPACE,
TEMPORARY_ERROR,
@ -18,10 +19,12 @@ import {
import { getRepoStatus } from '../../../util/git';
import * as hostRules from '../../../util/host-rules';
import { regEx } from '../../../util/regex';
import { Json } from '../../../util/schema-utils';
import { GitTagsDatasource } from '../../datasource/git-tags';
import { PackagistDatasource } from '../../datasource/packagist';
import type { UpdateArtifact, UpdateArtifactsResult } from '../types';
import type { AuthJson, ComposerLock } from './types';
import { Lockfile, PackageFile } from './schema';
import type { AuthJson } from './types';
import {
extractConstraints,
findGithubToken,
@ -105,10 +108,19 @@ export async function updateArtifacts({
}: UpdateArtifact): Promise<UpdateArtifactsResult[] | null> {
logger.debug(`composer.updateArtifacts(${packageFileName})`);
const file = Json.pipe(PackageFile).parse(newPackageFileContent);
const lockFileName = packageFileName.replace(regEx(/\.json$/), '.lock');
const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
if (!existingLockFileContent) {
logger.debug('No composer.lock found');
const lockfile = await z
.string()
.transform((f) => readLocalFile(f, 'utf8'))
.pipe(Json)
.pipe(Lockfile)
.nullable()
.catch(null)
.parseAsync(lockFileName);
if (!lockfile) {
logger.debug('Composer: unable to read lockfile');
return null;
}
@ -118,12 +130,8 @@ export async function updateArtifacts({
try {
await writeLocalFile(packageFileName, newPackageFileContent);
const existingLockFile: ComposerLock = JSON.parse(existingLockFileContent);
const constraints = {
...extractConstraints(
JSON.parse(newPackageFileContent),
existingLockFile
),
...extractConstraints(file, lockfile),
...config.constraints,
};
@ -150,7 +158,7 @@ export async function updateArtifacts({
const commands: string[] = [];
// Determine whether install is required before update
if (requireComposerDependencyInstallation(existingLockFile)) {
if (requireComposerDependencyInstallation(lockfile)) {
const preCmd = 'composer';
const preArgs =
'install' + getComposerArguments(config, composerToolConstraint);

View File

@ -279,7 +279,7 @@ describe('modules/manager/composer/extract', () => {
});
it('extracts dependencies with lock file', async () => {
fs.readLocalFile.mockResolvedValue('some content');
fs.readLocalFile.mockResolvedValue('{}');
const res = await extractPackageFile(requirements1, packageFile);
expect(res).toMatchSnapshot();
expect(res?.deps).toHaveLength(33);

View File

@ -1,215 +1,15 @@
import is from '@sindresorhus/is';
import { logger } from '../../../logger';
import { readLocalFile } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { GitTagsDatasource } from '../../datasource/git-tags';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import { PackagistDatasource } from '../../datasource/packagist';
import { api as semverComposer } from '../../versioning/composer';
import type { PackageDependency, PackageFileContent } from '../types';
import type {
ComposerConfig,
ComposerLock,
ComposerManagerData,
ComposerRepositories,
Repo,
} from './types';
/**
* The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies
* to use a base URL containing packages.json. Composer still works in this scenario by determining
* whether to add / remove packages.json from the URL.
*
* See https://github.com/composer/composer/blob/750a92b4b7aecda0e5b2f9b963f1cb1421900675/src/Composer/Repository/ComposerRepository.php#L815
*/
function transformRegUrl(url: string): string {
return url.replace(regEx(/(\/packages\.json)$/), '');
}
/**
* Parse the repositories field from a composer.json
*
* Entries with type vcs or git will be added to repositories,
* other entries will be added to registryUrls
*/
function parseRepositories(
repoJson: ComposerRepositories,
repositories: Record<string, Repo>,
registryUrls: string[]
): void {
try {
let packagist = true;
Object.entries(repoJson).forEach(([key, repo]) => {
if (is.object(repo)) {
const name = is.array(repoJson) ? repo.name : key;
switch (repo.type) {
case 'vcs':
case 'git':
case 'path':
repositories[name!] = repo;
break;
case 'composer':
registryUrls.push(transformRegUrl(repo.url));
break;
case 'package':
logger.debug(
{ url: repo.url },
'type package is not supported yet'
);
}
if (repo.packagist === false || repo['packagist.org'] === false) {
packagist = false;
}
} // istanbul ignore else: invalid repo
else if (['packagist', 'packagist.org'].includes(key) && repo === false) {
packagist = false;
}
});
if (packagist) {
registryUrls.push('https://packagist.org');
} else {
logger.debug('Disabling packagist.org');
}
} catch (e) /* istanbul ignore next */ {
logger.debug(
{ repositories: repoJson },
'Error parsing composer.json repositories config'
);
}
}
import type { PackageFileContent } from '../types';
import { ComposerExtract } from './schema';
export async function extractPackageFile(
content: string,
fileName: string
): Promise<PackageFileContent | null> {
logger.trace(`composer.extractPackageFile(${fileName})`);
let composerJson: ComposerConfig;
try {
composerJson = JSON.parse(content);
} catch (err) {
logger.debug(`Invalid JSON in ${fileName}`);
const res = await ComposerExtract.safeParseAsync({ content, fileName });
if (!res.success) {
logger.debug({ fileName, err: res.error }, 'Composer: extract failed');
return null;
}
const repositories: Record<string, Repo> = {};
const registryUrls: string[] = [];
const res: PackageFileContent = { deps: [] };
// handle lockfile
const lockfilePath = fileName.replace(regEx(/\.json$/), '.lock');
const lockContents = await readLocalFile(lockfilePath, 'utf8');
let lockParsed: ComposerLock | undefined;
if (lockContents) {
logger.debug(`Found composer lock file ${fileName}`);
res.lockFiles = [lockfilePath];
try {
lockParsed = JSON.parse(lockContents) as ComposerLock;
} catch (err) /* istanbul ignore next */ {
logger.warn({ err }, 'Error processing composer.lock');
}
}
// handle composer.json repositories
if (composerJson.repositories) {
parseRepositories(composerJson.repositories, repositories, registryUrls);
}
const deps: PackageDependency[] = [];
const depTypes: ('require' | 'require-dev')[] = ['require', 'require-dev'];
for (const depType of depTypes) {
if (composerJson[depType]) {
try {
for (const [depName, version] of Object.entries(
composerJson[depType]!
)) {
const currentValue = version.trim();
if (depName === 'php') {
deps.push({
depType,
depName,
currentValue,
datasource: GithubTagsDatasource.id,
packageName: 'php/php-src',
extractVersion: '^php-(?<version>.*)$',
});
} else {
// Default datasource and packageName
let datasource = PackagistDatasource.id;
let packageName = depName;
// Check custom repositories by type
if (repositories[depName]) {
switch (repositories[depName].type) {
case 'vcs':
case 'git':
datasource = GitTagsDatasource.id;
packageName = repositories[depName].url;
break;
case 'path':
deps.push({
depType,
depName,
currentValue,
skipReason: 'path-dependency',
});
continue;
}
}
const dep: PackageDependency = {
depType,
depName,
currentValue,
datasource,
};
if (depName !== packageName) {
dep.packageName = packageName;
}
if (!depName.includes('/')) {
dep.skipReason = 'unsupported';
}
if (lockParsed) {
const lockField =
depType === 'require'
? 'packages'
: /* istanbul ignore next */ 'packages-dev';
const lockedDep = lockParsed[lockField]?.find(
(item) => item.name === dep.depName
);
if (lockedDep && semverComposer.isVersion(lockedDep.version)) {
dep.lockedVersion = lockedDep.version.replace(regEx(/^v/i), '');
}
}
if (
!dep.skipReason &&
(!repositories[depName] ||
repositories[depName].type === 'composer') &&
registryUrls.length !== 0
) {
dep.registryUrls = registryUrls;
}
deps.push(dep);
}
}
} catch (err) /* istanbul ignore next */ {
logger.debug({ fileName, depType, err }, 'Error parsing composer.json');
return null;
}
}
}
if (!deps.length) {
return null;
}
res.deps = deps;
if (is.string(composerJson.type)) {
const managerData: ComposerManagerData = {
composerJsonType: composerJson.type,
};
res.managerData = managerData;
}
if (composerJson.require?.php) {
res.extractedConstraints = { php: composerJson.require.php };
}
return res;
return res.data;
}

View File

@ -0,0 +1,115 @@
import { Repos, ReposArray, ReposRecord } from './schema';
describe('modules/manager/composer/schema', () => {
describe('ReposRecord', () => {
it('parses default values', () => {
expect(ReposRecord.parse({})).toEqual([]);
});
it('parses repositories', () => {
expect(
ReposRecord.parse({
wpackagist: { type: 'composer', url: 'https://wpackagist.org' },
someGit: { type: 'vcs', url: 'https://some-vcs.com' },
somePath: { type: 'path', url: '/some/path' },
packagist: false,
'packagist.org': false,
foo: 'bar',
})
).toEqual([
{ type: 'composer', url: 'https://wpackagist.org' },
{ name: 'someGit', type: 'git', url: 'https://some-vcs.com' },
{ name: 'somePath', type: 'path', url: '/some/path' },
{ type: 'disable-packagist' },
{ type: 'disable-packagist' },
]);
});
});
describe('ReposArray', () => {
it('parses default values', () => {
expect(ReposArray.parse([])).toEqual([]);
});
it('parses repositories', () => {
expect(
ReposArray.parse([
{
type: 'composer',
url: 'https://wpackagist.org',
},
{ name: 'someGit', type: 'vcs', url: 'https://some-vcs.com' },
{ name: 'somePath', type: 'path', url: '/some/path' },
{ packagist: false },
{ 'packagist.org': false },
{ foo: 'bar' },
])
).toEqual([
{ type: 'composer', url: 'https://wpackagist.org' },
{ name: 'someGit', type: 'git', url: 'https://some-vcs.com' },
{ name: 'somePath', type: 'path', url: '/some/path' },
{ type: 'disable-packagist' },
{ type: 'disable-packagist' },
]);
});
});
describe('Repos', () => {
it('parses default values', () => {
expect(Repos.parse(null)).toEqual({
pathRepos: {},
gitRepos: {},
registryUrls: null,
});
});
it('parses repositories', () => {
expect(
Repos.parse([
{
name: 'wpackagist',
type: 'composer',
url: 'https://wpackagist.org',
},
{ name: 'someGit', type: 'vcs', url: 'https://some-vcs.com' },
{ name: 'somePath', type: 'path', url: '/some/path' },
])
).toEqual({
pathRepos: {
somePath: { name: 'somePath', type: 'path', url: '/some/path' },
},
registryUrls: ['https://wpackagist.org', 'https://packagist.org'],
gitRepos: {
someGit: {
name: 'someGit',
type: 'git',
url: 'https://some-vcs.com',
},
},
});
});
it(`parses repositories with packagist disabled`, () => {
expect(
Repos.parse({
wpackagist: { type: 'composer', url: 'https://wpackagist.org' },
someGit: { type: 'vcs', url: 'https://some-vcs.com' },
somePath: { type: 'path', url: '/some/path' },
packagist: false,
})
).toEqual({
pathRepos: {
somePath: { name: 'somePath', type: 'path', url: '/some/path' },
},
registryUrls: ['https://wpackagist.org'],
gitRepos: {
someGit: {
name: 'someGit',
type: 'git',
url: 'https://some-vcs.com',
},
},
});
});
});
});

View File

@ -0,0 +1,318 @@
import { z } from 'zod';
import { logger } from '../../../logger';
import { readLocalFile } from '../../../util/fs';
import { regEx } from '../../../util/regex';
import { Json, LooseArray, LooseRecord } from '../../../util/schema-utils';
import { GitTagsDatasource } from '../../datasource/git-tags';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import { PackagistDatasource } from '../../datasource/packagist';
import { api as semverComposer } from '../../versioning/composer';
import type { PackageDependency, PackageFileContent } from '../types';
import type { ComposerManagerData } from './types';
export const ComposerRepo = z.object({
type: z.literal('composer'),
/**
* The regUrl is expected to be a base URL. GitLab composer repository installation guide specifies
* to use a base URL containing packages.json. Composer still works in this scenario by determining
* whether to add / remove packages.json from the URL.
*
* See https://github.com/composer/composer/blob/750a92b4b7aecda0e5b2f9b963f1cb1421900675/src/Composer/Repository/ComposerRepository.php#L815
*/
url: z.string().transform((url) => url.replace(/\/packages\.json$/, '')),
});
export type ComposerRepo = z.infer<typeof ComposerRepo>;
export const GitRepo = z.object({
type: z.enum(['vcs', 'git']).transform(() => 'git' as const),
url: z.string(),
});
export type GitRepo = z.infer<typeof GitRepo>;
export const PathRepo = z.object({
type: z.literal('path'),
url: z.string(),
});
export type PathRepo = z.infer<typeof PathRepo>;
export const Repo = z.discriminatedUnion('type', [
ComposerRepo,
GitRepo,
PathRepo,
]);
export type Repo = z.infer<typeof ComposerRepo>;
export const NamedRepo = z.discriminatedUnion('type', [
ComposerRepo,
GitRepo.extend({ name: z.string() }),
PathRepo.extend({ name: z.string() }),
]);
export type NamedRepo = z.infer<typeof NamedRepo>;
const DisablePackagist = z.object({ type: z.literal('disable-packagist') });
export type DisablePackagist = z.infer<typeof DisablePackagist>;
export const ReposRecord = LooseRecord(z.union([Repo, z.literal(false)]), {
onError: ({ error: err }) => {
logger.warn({ err }, 'Composer: error parsing repositories object');
},
}).transform((repos) => {
const result: (NamedRepo | DisablePackagist)[] = [];
for (const [name, repo] of Object.entries(repos)) {
if (repo === false) {
if (name === 'packagist' || name === 'packagist.org') {
result.push({ type: 'disable-packagist' });
}
continue;
}
if (repo.type === 'path' || repo.type === 'git') {
result.push({ name, ...repo });
continue;
}
if (repo.type === 'composer') {
result.push(repo);
continue;
}
}
return result;
});
export type ReposRecord = z.infer<typeof ReposRecord>;
export const ReposArray = LooseArray(
z.union([
NamedRepo,
z
.union([
z.object({ packagist: z.literal(false) }),
z.object({ 'packagist.org': z.literal(false) }),
])
.transform((): DisablePackagist => ({ type: 'disable-packagist' })),
]),
{
onError: ({ error: err }) => {
logger.warn({ err }, 'Composer: error parsing repositories array');
},
}
).transform((repos) => repos.filter((x): x is NamedRepo => x !== null));
export type ReposArray = z.infer<typeof ReposArray>;
export const Repos = z
.union([ReposRecord, ReposArray])
.default([]) // Prevents warnings for packages without repositories field
.catch(({ error: err }) => {
logger.warn({ err }, 'Composer: repositories parsing error');
return [];
})
.transform((repos) => {
let packagist = true;
const repoUrls: string[] = [];
const gitRepos: Record<string, GitRepo> = {};
const pathRepos: Record<string, PathRepo> = {};
for (const repo of repos) {
if (repo.type === 'composer') {
repoUrls.push(repo.url);
} else if (repo.type === 'git') {
gitRepos[repo.name] = repo;
} else if (repo.type === 'path') {
pathRepos[repo.name] = repo;
} else if (repo.type === 'disable-packagist') {
packagist = false;
}
}
if (packagist && repoUrls.length) {
repoUrls.push('https://packagist.org');
}
const registryUrls = repoUrls.length ? repoUrls : null;
return { registryUrls, gitRepos, pathRepos };
});
export type Repos = z.infer<typeof Repos>;
const RequireDefs = LooseRecord(z.string().transform((x) => x.trim())).catch(
{}
);
export const PackageFile = z
.object({
type: z.string().optional(),
config: z
.object({
platform: z.object({
php: z.string(),
}),
})
.nullable()
.catch(null),
repositories: Repos,
require: RequireDefs,
'require-dev': RequireDefs,
})
.transform(
({
type: composerJsonType,
config,
repositories,
require,
'require-dev': requireDev,
}) => ({
composerJsonType,
config,
repositories,
require,
requireDev,
})
);
export type PackageFile = z.infer<typeof PackageFile>;
const LockedPackage = z.object({
name: z.string(),
version: z.string(),
});
type LockedPackage = z.infer<typeof LockedPackage>;
export const Lockfile = z
.object({
'plugin-api-version': z.string().optional(),
packages: LooseArray(LockedPackage).catch([]),
'packages-dev': LooseArray(LockedPackage).catch([]),
})
.transform(
({
'plugin-api-version': pluginApiVersion,
packages,
'packages-dev': packagesDev,
}) => ({ pluginApiVersion, packages, packagesDev })
);
export type Lockfile = z.infer<typeof Lockfile>;
export const ComposerExtract = z
.object({
content: z.string(),
fileName: z.string(),
})
.transform(({ content, fileName }) => {
const lockfileName = fileName.replace(/\.json$/, '.lock');
return {
file: content,
lockfileName,
lockfile: lockfileName,
};
})
.pipe(
z.object({
file: Json.pipe(PackageFile),
lockfileName: z.string(),
lockfile: z
.string()
.transform((lockfileName) => readLocalFile(lockfileName, 'utf8'))
.pipe(Json)
.pipe(Lockfile)
.nullable()
.catch(({ error: err }) => {
logger.warn({ err }, 'Composer: lockfile parsing error');
return null;
}),
})
)
.transform(({ file, lockfile, lockfileName }) => {
const { composerJsonType, require, requireDev } = file;
const { registryUrls, gitRepos, pathRepos } = file.repositories;
const deps: PackageDependency[] = [];
const profiles = [
{
depType: 'require',
req: require,
locked: lockfile?.packages ?? [],
},
{
depType: 'require-dev',
req: requireDev,
locked: lockfile?.packagesDev ?? [],
},
];
for (const { depType, req, locked } of profiles) {
for (const [depName, currentValue] of Object.entries(req)) {
if (depName === 'php') {
deps.push({
depType,
depName,
currentValue,
datasource: GithubTagsDatasource.id,
packageName: 'php/php-src',
extractVersion: '^php-(?<version>.*)$',
});
continue;
}
if (pathRepos[depName]) {
deps.push({
depType,
depName,
currentValue,
skipReason: 'path-dependency',
});
continue;
}
const dep: PackageDependency = {
depType,
depName,
currentValue,
};
if (!depName.includes('/')) {
dep.skipReason = 'unsupported';
}
const lockedDep = locked.find((item) => item.name === depName);
if (lockedDep && semverComposer.isVersion(lockedDep.version)) {
dep.lockedVersion = lockedDep.version.replace(regEx(/^v/i), '');
}
const gitRepo = gitRepos[depName];
if (gitRepo) {
dep.datasource = GitTagsDatasource.id;
dep.packageName = gitRepo.url;
deps.push(dep);
continue;
}
dep.datasource = PackagistDatasource.id;
if (registryUrls) {
dep.registryUrls = registryUrls;
}
deps.push(dep);
}
}
if (!deps.length) {
return null;
}
const res: PackageFileContent<ComposerManagerData> = { deps };
if (composerJsonType) {
res.managerData = { composerJsonType };
}
if (require.php) {
res.extractedConstraints = { php: require.php };
}
if (lockfile) {
res.lockFiles = [lockfileName];
}
return res;
});
export type ComposerExtract = z.infer<typeof ComposerExtract>;

View File

@ -1,49 +1,3 @@
// istanbul ignore file: types only
export interface Repo {
name?: string;
type: 'composer' | 'git' | 'package' | 'path' | 'vcs';
packagist?: boolean;
'packagist.org'?: boolean;
url: string;
}
export type ComposerRepositories = Record<string, Repo | boolean> | Repo[];
export interface ComposerConfig {
type?: string;
/**
* Setting a fixed PHP version (e.g. {"php": "7.0.3"}) will let you fake the
* platform version so that you can emulate a production env or define your
* target platform in the config.
* See https://getcomposer.org/doc/06-config.md#platform
*/
config?: {
platform?: {
php?: string;
};
};
/**
* A repositories field can be an array of Repo objects or an object of repoName: Repo
* Also it can be a boolean (usually false) to disable packagist.
* (Yes this can be confusing, as it is also not properly documented in the composer docs)
* See https://getcomposer.org/doc/05-repositories.md#disabling-packagist-org
*/
repositories?: ComposerRepositories;
require?: Record<string, string>;
'require-dev'?: Record<string, string>;
}
export interface ComposerLockPackage {
name: string;
version: string;
}
export interface ComposerLock {
'plugin-api-version'?: string;
packages?: ComposerLockPackage[];
'packages-dev'?: ComposerLockPackage[];
}
export interface ComposerManagerData {
composerJsonType?: string;
}

View File

@ -1,7 +1,8 @@
import { logger } from '../../../logger';
import { Json } from '../../../util/schema-utils';
import { api as composer } from '../../versioning/composer';
import type { UpdateLockedConfig, UpdateLockedResult } from '../types';
import type { ComposerLock } from './types';
import { Lockfile } from './schema';
export function updateLockedDependency(
config: UpdateLockedConfig
@ -12,12 +13,11 @@ export function updateLockedDependency(
`composer.updateLockedDependency: ${depName}@${currentVersion} -> ${newVersion} [${lockFile}]`
);
try {
const locked = JSON.parse(lockFileContent!) as ComposerLock;
const lockfile = Json.pipe(Lockfile).parse(lockFileContent);
if (
locked.packages?.find(
(entry) =>
entry.name === depName &&
composer.equals(entry.version || '', newVersion)
lockfile?.packages.find(
({ name, version }) =>
name === depName && composer.equals(version, newVersion)
)
) {
return { status: 'already-updated' };

View File

@ -1,6 +1,7 @@
import { GlobalConfig } from '../../../config/global';
import * as hostRules from '../../../util/host-rules';
import { GitTagsDatasource } from '../../datasource/git-tags';
import { Lockfile, PackageFile } from './schema';
import {
extractConstraints,
findGithubToken,
@ -21,114 +22,121 @@ describe('modules/manager/composer/utils', () => {
describe('extractConstraints', () => {
it('returns from require', () => {
expect(
extractConstraints(
{ require: { php: '>=5.3.2', 'composer/composer': '1.1.0' } },
{}
)
).toEqual({ php: '>=5.3.2', composer: '1.1.0' });
const file = PackageFile.parse({
require: { php: '>=5.3.2', 'composer/composer': '1.1.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
php: '>=5.3.2',
composer: '1.1.0',
});
});
it('returns platform php version', () => {
expect(
extractConstraints(
{
config: { platform: { php: '7.4.27' } },
require: { php: '~7.4 || ~8.0' },
},
{}
)
).toEqual({ composer: '1.*', php: '<=7.4.27' });
const file = PackageFile.parse({
config: { platform: { php: '7.4.27' } },
require: { php: '~7.4 || ~8.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
composer: '1.*',
php: '<=7.4.27',
});
});
it('returns platform 0 minor php version', () => {
expect(
extractConstraints(
{
config: { platform: { php: '7.0.5' } },
require: { php: '^7.0 || ~8.0' },
},
{}
)
).toEqual({ composer: '1.*', php: '<=7.0.5' });
const file = PackageFile.parse({
config: { platform: { php: '7.0.5' } },
require: { php: '^7.0 || ~8.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
composer: '1.*',
php: '<=7.0.5',
});
});
it('returns platform 0 patch php version', () => {
expect(
extractConstraints(
{
config: { platform: { php: '7.4.0' } },
require: { php: '^7.0 || ~8.0' },
},
{}
)
).toEqual({ composer: '1.*', php: '<=7.4.0' });
const file = PackageFile.parse({
config: { platform: { php: '7.4.0' } },
require: { php: '^7.0 || ~8.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
composer: '1.*',
php: '<=7.4.0',
});
});
it('returns platform lowest minor php version', () => {
expect(
extractConstraints(
{
config: { platform: { php: '7' } },
require: { php: '^7.0 || ~8.0' },
},
{}
)
).toEqual({ composer: '1.*', php: '<=7.0.0' });
const file = PackageFile.parse({
config: { platform: { php: '7' } },
require: { php: '^7.0 || ~8.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
composer: '1.*',
php: '<=7.0.0',
});
});
it('returns platform lowest patch php version', () => {
expect(
extractConstraints(
{
config: { platform: { php: '7.4' } },
require: { php: '~7.4 || ~8.0' },
},
{}
)
).toEqual({ composer: '1.*', php: '<=7.4.0' });
const file = PackageFile.parse({
config: { platform: { php: '7.4' } },
require: { php: '~7.4 || ~8.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
composer: '1.*',
php: '<=7.4.0',
});
});
it('returns from require-dev', () => {
expect(
extractConstraints(
{ 'require-dev': { 'composer/composer': '1.1.0' } },
{}
)
).toEqual({ composer: '1.1.0' });
const file = PackageFile.parse({
'require-dev': { 'composer/composer': '1.1.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({ composer: '1.1.0' });
});
it('returns from composer platform require', () => {
expect(
extractConstraints({ require: { php: '^8.1', composer: '2.2.0' } }, {})
).toEqual({ php: '^8.1', composer: '2.2.0' });
const file = PackageFile.parse({
require: { php: '^8.1', composer: '2.2.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({
php: '^8.1',
composer: '2.2.0',
});
});
it('returns from composer platform require-dev', () => {
expect(
extractConstraints({ 'require-dev': { composer: '^2.2' } }, {})
).toEqual({ composer: '^2.2' });
const file = PackageFile.parse({ 'require-dev': { composer: '^2.2' } });
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({ composer: '^2.2' });
});
it('returns from composer-runtime-api', () => {
expect(
extractConstraints(
{ require: { 'composer-runtime-api': '^1.1.0' } },
{}
)
).toEqual({ composer: '^1.1' });
const file = PackageFile.parse({
require: { 'composer-runtime-api': '^1.1.0' },
});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({ composer: '^1.1' });
});
it('returns from plugin-api-version', () => {
expect(extractConstraints({}, { 'plugin-api-version': '1.1.0' })).toEqual(
{
composer: '^1.1',
}
);
const file = PackageFile.parse({});
const lockfile = Lockfile.parse({ 'plugin-api-version': '1.1.0' });
expect(extractConstraints(file, lockfile)).toEqual({
composer: '^1.1',
});
});
it('fallback to 1.*', () => {
expect(extractConstraints({}, {})).toEqual({ composer: '1.*' });
const file = PackageFile.parse({});
const lockfile = Lockfile.parse({});
expect(extractConstraints(file, lockfile)).toEqual({ composer: '1.*' });
});
});
@ -276,27 +284,24 @@ describe('modules/manager/composer/utils', () => {
describe('requireComposerDependencyInstallation', () => {
it('returns true when symfony/flex has been installed', () => {
expect(
requireComposerDependencyInstallation({
packages: [{ name: 'symfony/flex', version: '1.17.1' }],
})
).toBeTrue();
const lockfile = Lockfile.parse({
packages: [{ name: 'symfony/flex', version: '1.17.1' }],
});
expect(requireComposerDependencyInstallation(lockfile)).toBeTrue();
});
it('returns true when symfony/flex has been installed as dev dependency', () => {
expect(
requireComposerDependencyInstallation({
'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }],
})
).toBeTrue();
const lockfile = Lockfile.parse({
'packages-dev': [{ name: 'symfony/flex', version: '1.17.1' }],
});
expect(requireComposerDependencyInstallation(lockfile)).toBeTrue();
});
it('returns false when symfony/flex has not been installed', () => {
expect(
requireComposerDependencyInstallation({
packages: [{ name: 'symfony/console', version: '5.4.0' }],
})
).toBeFalse();
const lockfile = Lockfile.parse({
packages: [{ name: 'symfony/console', version: '5.4.0' }],
});
expect(requireComposerDependencyInstallation(lockfile)).toBeFalse();
});
});

View File

@ -7,7 +7,7 @@ import type { HostRuleSearchResult } from '../../../types';
import type { ToolConstraint } from '../../../util/exec/types';
import { api, id as composerVersioningId } from '../../versioning/composer';
import type { UpdateArtifactsConfig } from '../types';
import type { ComposerConfig, ComposerLock } from './types';
import type { Lockfile, PackageFile } from './schema';
export { composerVersioningId };
@ -59,53 +59,55 @@ export function getPhpConstraint(
return null;
}
export function requireComposerDependencyInstallation(
lock: ComposerLock
): boolean {
export function requireComposerDependencyInstallation({
packages,
packagesDev,
}: Lockfile): boolean {
return (
lock.packages?.some((p) => depRequireInstall.has(p.name)) === true ||
lock['packages-dev']?.some((p) => depRequireInstall.has(p.name)) === true
packages.some((p) => depRequireInstall.has(p.name)) === true ||
packagesDev.some((p) => depRequireInstall.has(p.name)) === true
);
}
export function extractConstraints(
composerJson: ComposerConfig,
lockParsed: ComposerLock
{ config, require, requireDev }: PackageFile,
{ pluginApiVersion }: Lockfile
): Record<string, string> {
const res: Record<string, string> = { composer: '1.*' };
// extract php
if (composerJson.config?.platform?.php) {
const major = api.getMajor(composerJson.config.platform.php);
const minor = api.getMinor(composerJson.config.platform.php) ?? 0;
const patch = api.getPatch(composerJson.config.platform.php) ?? 0;
const phpVersion = config?.platform.php;
if (phpVersion) {
const major = api.getMajor(phpVersion);
const minor = api.getMinor(phpVersion) ?? 0;
const patch = api.getPatch(phpVersion) ?? 0;
res.php = `<=${major}.${minor}.${patch}`;
} else if (composerJson.require?.php) {
res.php = composerJson.require.php;
} else if (require.php) {
res.php = require.php;
}
// extract direct composer dependency
if (composerJson.require?.['composer/composer']) {
res.composer = composerJson.require?.['composer/composer'];
} else if (composerJson['require-dev']?.['composer/composer']) {
res.composer = composerJson['require-dev']?.['composer/composer'];
if (require['composer/composer']) {
res.composer = require['composer/composer'];
} else if (requireDev['composer/composer']) {
res.composer = requireDev['composer/composer'];
}
// composer platform package
else if (composerJson.require?.['composer']) {
res.composer = composerJson.require?.['composer'];
} else if (composerJson['require-dev']?.['composer']) {
res.composer = composerJson['require-dev']?.['composer'];
else if (require['composer']) {
res.composer = require['composer'];
} else if (requireDev['composer']) {
res.composer = requireDev['composer'];
}
// check last used composer version
else if (lockParsed?.['plugin-api-version']) {
const major = api.getMajor(lockParsed?.['plugin-api-version']);
const minor = api.getMinor(lockParsed?.['plugin-api-version']);
else if (pluginApiVersion) {
const major = api.getMajor(pluginApiVersion);
const minor = api.getMinor(pluginApiVersion);
res.composer = `^${major}.${minor}`;
}
// check composer api dependency
else if (composerJson.require?.['composer-runtime-api']) {
const major = api.getMajor(composerJson.require?.['composer-runtime-api']);
const minor = api.getMinor(composerJson.require?.['composer-runtime-api']);
else if (require['composer-runtime-api']) {
const major = api.getMajor(require['composer-runtime-api']);
const minor = api.getMinor(require['composer-runtime-api']);
res.composer = `^${major}.${minor}`;
}
return res;