Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(composer): Use schema for parsing #21520

Merged
merged 25 commits into from May 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a85b64b
refactor(composer): Use schema for parsing
zharinov Apr 14, 2023
ee0e177
Merge branch 'main' into refactor/composer-manager-schema
zharinov Apr 14, 2023
23b5b67
Fix
zharinov Apr 14, 2023
ec91066
Provide default repo list to reduce verbosity
zharinov Apr 14, 2023
70fcef5
Fix coverage
zharinov Apr 14, 2023
e2d2793
Merge branch 'main' into refactor/composer-manager-schema
zharinov Apr 16, 2023
33251d1
refactor
zharinov Apr 16, 2023
1530766
Fixes
zharinov Apr 16, 2023
b350fa0
feat(schema): Better utility for JSON parsing
zharinov Apr 16, 2023
b6aecf0
Test invalid json
zharinov Apr 16, 2023
cf63f1e
Simplify
zharinov Apr 16, 2023
e57414f
Merge branch 'feat/schema-util-json' into refactor/composer-manager-s…
zharinov Apr 16, 2023
05ae518
Adjust to Json schema util
zharinov Apr 16, 2023
8aac47b
Merge branch 'main' into refactor/composer-manager-schema
zharinov Apr 21, 2023
dff4deb
Fix merge
zharinov Apr 21, 2023
f830b42
Add comment
zharinov Apr 21, 2023
f164786
Refactor url transform
zharinov Apr 21, 2023
5b32b28
More refactoring
zharinov Apr 21, 2023
1cbcf6c
Fix coverage
zharinov Apr 21, 2023
d8668b6
Insert packagist URL as the first registryUrl
zharinov Apr 21, 2023
4ac06b7
Merge branch 'main' into refactor/composer-manager-schema
zharinov May 6, 2023
e1aa75e
Fix registry urls
zharinov May 6, 2023
bdcac79
Fix snapshot
zharinov May 6, 2023
0ffbb6c
Don't change `registryUrl` order
zharinov May 6, 2023
aa321cc
Merge branch 'main' into refactor/composer-manager-schema
zharinov May 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}