Skip to content

Commit

Permalink
refactor(composer): Use schema for parsing (#21520)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed May 7, 2023
1 parent 546a52c commit 664dc80
Show file tree
Hide file tree
Showing 9 changed files with 589 additions and 387 deletions.
28 changes: 18 additions & 10 deletions lib/modules/manager/composer/artifacts.ts
@@ -1,5 +1,6 @@
import is from '@sindresorhus/is';
import { quote } from 'shlex';
import { z } from 'zod';
import {
SYSTEM_INSUFFICIENT_DISK_SPACE,
TEMPORARY_ERROR,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}

Expand All @@ -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,
};

Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion lib/modules/manager/composer/extract.spec.ts
Expand Up @@ -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);
Expand Down
212 changes: 6 additions & 206 deletions lib/modules/manager/composer/extract.ts
@@ -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;
}

0 comments on commit 664dc80

Please sign in to comment.