Skip to content

Commit fc697a3

Browse files
mkniewallnerviceicerarkins
authoredOct 29, 2024··
refactor(manager/cargo): use zod to parse manifest/config (#31260)
Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: Rhys Arkins <rhys@arkins.net>
1 parent 3ea0a39 commit fc697a3

File tree

7 files changed

+221
-161
lines changed

7 files changed

+221
-161
lines changed
 

‎lib/modules/manager/cargo/__snapshots__/extract.spec.ts.snap

+9
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() extracts registry ur
461461
"depType": "dependencies",
462462
"managerData": {
463463
"nestedVersion": true,
464+
"registryName": "private-crates",
464465
},
465466
"registryUrls": [
466467
"https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git",
@@ -473,6 +474,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() extracts registry ur
473474
"depType": "dependencies",
474475
"managerData": {
475476
"nestedVersion": true,
477+
"registryName": "mcorbin",
476478
},
477479
"registryUrls": [
478480
"https://github.com/mcorbin/testregistry",
@@ -499,6 +501,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() extracts registry ur
499501
"depType": "dependencies",
500502
"managerData": {
501503
"nestedVersion": true,
504+
"registryName": "private-crates",
502505
},
503506
"registryUrls": [
504507
"https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git",
@@ -511,6 +514,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() extracts registry ur
511514
"depType": "dependencies",
512515
"managerData": {
513516
"nestedVersion": true,
517+
"registryName": "mcorbin",
514518
},
515519
"registryUrls": [
516520
"https://github.com/mcorbin/testregistry",
@@ -537,6 +541,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() fails to parse cargo
537541
"depType": "dependencies",
538542
"managerData": {
539543
"nestedVersion": true,
544+
"registryName": "private-crates",
540545
},
541546
"skipReason": "unknown-registry",
542547
},
@@ -547,6 +552,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() fails to parse cargo
547552
"depType": "dependencies",
548553
"managerData": {
549554
"nestedVersion": true,
555+
"registryName": "mcorbin",
550556
},
551557
"skipReason": "unknown-registry",
552558
},
@@ -718,6 +724,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() ignore cargo config
718724
"depType": "dependencies",
719725
"managerData": {
720726
"nestedVersion": true,
727+
"registryName": "private-crates",
721728
},
722729
"skipReason": "unknown-registry",
723730
},
@@ -728,6 +735,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() ignore cargo config
728735
"depType": "dependencies",
729736
"managerData": {
730737
"nestedVersion": true,
738+
"registryName": "mcorbin",
731739
},
732740
"skipReason": "unknown-registry",
733741
},
@@ -752,6 +760,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() skips unknown regist
752760
"depType": "dependencies",
753761
"managerData": {
754762
"nestedVersion": true,
763+
"registryName": "not-listed",
755764
},
756765
"skipReason": "unknown-registry",
757766
},

‎lib/modules/manager/cargo/extract.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ replace-with = "private-crates"`,
136136
depType: 'dependencies',
137137
managerData: {
138138
nestedVersion: true,
139+
registryName: 'private-crates',
139140
},
140141
registryUrls: [
141142
'https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git',
@@ -148,6 +149,7 @@ replace-with = "private-crates"`,
148149
depType: 'dependencies',
149150
managerData: {
150151
nestedVersion: true,
152+
registryName: 'mcorbin',
151153
},
152154
registryUrls: [
153155
'https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git',
@@ -212,6 +214,7 @@ replace-with = "mcorbin"`,
212214
depType: 'dependencies',
213215
managerData: {
214216
nestedVersion: true,
217+
registryName: 'private-crates',
215218
},
216219
},
217220
{
@@ -221,6 +224,7 @@ replace-with = "mcorbin"`,
221224
depType: 'dependencies',
222225
managerData: {
223226
nestedVersion: true,
227+
registryName: 'mcorbin',
224228
},
225229
},
226230
{
@@ -302,6 +306,7 @@ replace-with = "mcorbin"`,
302306
depType: 'dependencies',
303307
managerData: {
304308
nestedVersion: true,
309+
registryName: 'private-crates',
305310
},
306311
registryUrls: [
307312
'https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git',
@@ -314,6 +319,7 @@ replace-with = "mcorbin"`,
314319
depType: 'dependencies',
315320
managerData: {
316321
nestedVersion: true,
322+
registryName: 'mcorbin',
317323
},
318324
registryUrls: ['https://github.com/mcorbin/testregistry'],
319325
},
@@ -437,6 +443,7 @@ replace-with = "mine"`,
437443
depType: 'dependencies',
438444
managerData: {
439445
nestedVersion: true,
446+
registryName: 'private-crates',
440447
},
441448
skipReason: 'unknown-registry',
442449
},
@@ -447,6 +454,7 @@ replace-with = "mine"`,
447454
depType: 'dependencies',
448455
managerData: {
449456
nestedVersion: true,
457+
registryName: 'mcorbin',
450458
},
451459
skipReason: 'unknown-registry',
452460
},
@@ -493,6 +501,7 @@ replace-with = "mcorbin"
493501
depType: 'dependencies',
494502
managerData: {
495503
nestedVersion: true,
504+
registryName: 'private-crates',
496505
},
497506
skipReason: 'unknown-registry',
498507
},
@@ -503,6 +512,7 @@ replace-with = "mcorbin"
503512
depType: 'dependencies',
504513
managerData: {
505514
nestedVersion: true,
515+
registryName: 'mcorbin',
506516
},
507517
skipReason: 'unknown-registry',
508518
},

‎lib/modules/manager/cargo/extract.ts

+48-97
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { logger } from '../../../logger';
2-
import type { SkipReason } from '../../../types';
32
import { coerceArray } from '../../../util/array';
43
import { findLocalSiblingOrParent, readLocalFile } from '../../../util/fs';
5-
import { parse as parseToml } from '../../../util/toml';
6-
import { CrateDatasource } from '../../datasource/crate';
74
import { api as versioning } from '../../versioning/cargo';
85
import type {
96
ExtractConfig,
107
PackageDependency,
118
PackageFileContent,
129
} from '../types';
1310
import { extractLockFileVersions } from './locked-version';
11+
import {
12+
type CargoConfig,
13+
CargoConfigSchema,
14+
CargoManifestSchema,
15+
} from './schema';
1416
import type {
15-
CargoConfig,
16-
CargoManifest,
17+
CargoManagerData,
1718
CargoRegistries,
1819
CargoRegistryUrl,
19-
CargoSection,
2020
} from './types';
2121
import { DEFAULT_REGISTRY_URL } from './utils';
2222

@@ -28,75 +28,32 @@ function getCargoIndexEnv(registryName: string): string | null {
2828
}
2929

3030
function extractFromSection(
31-
parsedContent: CargoSection,
32-
section: keyof CargoSection,
31+
dependencies: PackageDependency<CargoManagerData>[] | undefined,
3332
cargoRegistries: CargoRegistries,
3433
target?: string,
35-
depTypeOverride?: string,
3634
): PackageDependency[] {
37-
const deps: PackageDependency[] = [];
38-
const sectionContent = parsedContent[section];
39-
if (!sectionContent) {
35+
if (!dependencies) {
4036
return [];
4137
}
42-
Object.keys(sectionContent).forEach((depName) => {
43-
let skipReason: SkipReason | undefined;
44-
let currentValue = sectionContent[depName];
45-
let nestedVersion = false;
46-
let registryUrls: string[] | undefined;
47-
let packageName: string | undefined;
48-
49-
if (typeof currentValue !== 'string') {
50-
const version = currentValue.version;
51-
const path = currentValue.path;
52-
const git = currentValue.git;
53-
const registryName = currentValue.registry;
54-
const workspace = currentValue.workspace;
5538

56-
packageName = currentValue.package;
39+
const deps: PackageDependency<CargoManagerData>[] = [];
5740

58-
if (version) {
59-
currentValue = version;
60-
nestedVersion = true;
61-
if (registryName) {
62-
const registryUrl =
63-
getCargoIndexEnv(registryName) ?? cargoRegistries[registryName];
41+
for (const dep of Object.values(dependencies)) {
42+
let registryUrls: string[] | undefined;
6443

65-
if (registryUrl) {
66-
if (registryUrl !== DEFAULT_REGISTRY_URL) {
67-
registryUrls = [registryUrl];
68-
}
69-
} else {
70-
skipReason = 'unknown-registry';
71-
}
72-
}
73-
if (path) {
74-
skipReason = 'path-dependency';
75-
}
76-
if (git) {
77-
skipReason = 'git-dependency';
44+
if (dep.managerData?.registryName) {
45+
const registryUrl =
46+
getCargoIndexEnv(dep.managerData.registryName) ??
47+
cargoRegistries[dep.managerData?.registryName];
48+
if (registryUrl) {
49+
if (registryUrl !== DEFAULT_REGISTRY_URL) {
50+
registryUrls = [registryUrl];
7851
}
79-
} else if (path) {
80-
currentValue = '';
81-
skipReason = 'path-dependency';
82-
} else if (git) {
83-
currentValue = '';
84-
skipReason = 'git-dependency';
85-
} else if (workspace) {
86-
currentValue = '';
87-
skipReason = 'inherited-dependency';
8852
} else {
89-
currentValue = '';
90-
skipReason = 'invalid-dependency-specification';
53+
dep.skipReason = 'unknown-registry';
9154
}
9255
}
93-
const dep: PackageDependency = {
94-
depName,
95-
depType: section,
96-
currentValue: currentValue as any,
97-
managerData: { nestedVersion },
98-
datasource: CrateDatasource.id,
99-
};
56+
10057
if (registryUrls) {
10158
dep.registryUrls = registryUrls;
10259
} else {
@@ -108,24 +65,16 @@ function extractFromSection(
10865
} else {
10966
// we always expect to have DEFAULT_REGISTRY_ID set, if it's not it means the config defines an alternative
11067
// registry that we couldn't resolve.
111-
skipReason = 'unknown-registry';
68+
dep.skipReason = 'unknown-registry';
11269
}
11370
}
11471

115-
if (skipReason) {
116-
dep.skipReason = skipReason;
117-
}
11872
if (target) {
11973
dep.target = target;
12074
}
121-
if (packageName) {
122-
dep.packageName = packageName;
123-
}
124-
if (depTypeOverride) {
125-
dep.depType = depTypeOverride;
126-
}
12775
deps.push(dep);
128-
});
76+
}
77+
12978
return deps;
13079
}
13180

@@ -135,12 +84,15 @@ async function readCargoConfig(): Promise<CargoConfig | null> {
13584
const path = `.cargo/${configName}`;
13685
const payload = await readLocalFile(path, 'utf8');
13786
if (payload) {
138-
try {
139-
return parseToml(payload) as CargoConfig;
140-
} catch (err) {
141-
logger.debug({ err }, `Error parsing ${path}`);
87+
const parsedCargoConfig = CargoConfigSchema.safeParse(payload);
88+
if (parsedCargoConfig.success) {
89+
return parsedCargoConfig.data;
90+
} else {
91+
logger.debug(
92+
{ err: parsedCargoConfig.error, path },
93+
`Error parsing cargo config`,
94+
);
14295
}
143-
break;
14496
}
14597
}
14698

@@ -217,19 +169,23 @@ export async function extractPackageFile(
217169
content: string,
218170
packageFile: string,
219171
_config?: ExtractConfig,
220-
): Promise<PackageFileContent | null> {
172+
): Promise<PackageFileContent<CargoManagerData> | null> {
221173
logger.trace(`cargo.extractPackageFile(${packageFile})`);
222174

223175
const cargoConfig = (await readCargoConfig()) ?? {};
224176
const cargoRegistries = extractCargoRegistries(cargoConfig);
225177

226-
let cargoManifest: CargoManifest;
227-
try {
228-
cargoManifest = parseToml(content) as CargoManifest;
229-
} catch (err) {
230-
logger.debug({ err, packageFile }, 'Error parsing Cargo.toml file');
178+
const parsedCargoManifest = CargoManifestSchema.safeParse(content);
179+
if (!parsedCargoManifest.success) {
180+
logger.debug(
181+
{ err: parsedCargoManifest.error, packageFile },
182+
'Error parsing Cargo.toml file',
183+
);
231184
return null;
232185
}
186+
187+
const cargoManifest = parsedCargoManifest.data;
188+
233189
/*
234190
There are the following sections in Cargo.toml:
235191
[package]
@@ -249,20 +205,17 @@ export async function extractPackageFile(
249205
// Dependencies for `${target}`
250206
const deps = [
251207
...extractFromSection(
252-
targetContent,
253-
'dependencies',
208+
targetContent.dependencies,
254209
cargoRegistries,
255210
target,
256211
),
257212
...extractFromSection(
258-
targetContent,
259-
'dev-dependencies',
213+
targetContent['dev-dependencies'],
260214
cargoRegistries,
261215
target,
262216
),
263217
...extractFromSection(
264-
targetContent,
265-
'build-dependencies',
218+
targetContent['build-dependencies'],
266219
cargoRegistries,
267220
target,
268221
),
@@ -275,18 +228,16 @@ export async function extractPackageFile(
275228
let workspaceDeps: PackageDependency[] = [];
276229
if (workspaceSection) {
277230
workspaceDeps = extractFromSection(
278-
workspaceSection,
279-
'dependencies',
231+
workspaceSection.dependencies,
280232
cargoRegistries,
281233
undefined,
282-
'workspace.dependencies',
283234
);
284235
}
285236

286237
const deps = [
287-
...extractFromSection(cargoManifest, 'dependencies', cargoRegistries),
288-
...extractFromSection(cargoManifest, 'dev-dependencies', cargoRegistries),
289-
...extractFromSection(cargoManifest, 'build-dependencies', cargoRegistries),
238+
...extractFromSection(cargoManifest.dependencies, cargoRegistries),
239+
...extractFromSection(cargoManifest['dev-dependencies'], cargoRegistries),
240+
...extractFromSection(cargoManifest['build-dependencies'], cargoRegistries),
290241
...targetDeps,
291242
...workspaceDeps,
292243
];

‎lib/modules/manager/cargo/schema.ts

+129-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,133 @@
11
import { z } from 'zod';
2-
import { Toml } from '../../../util/schema-utils';
2+
import type { SkipReason } from '../../../types';
3+
import { Toml, withDepType } from '../../../util/schema-utils';
4+
import { CrateDatasource } from '../../datasource/crate';
5+
import type { PackageDependency } from '../types';
6+
import type { CargoManagerData } from './types';
7+
8+
const CargoDep = z.union([
9+
z
10+
.object({
11+
/** Path on disk to the crate sources */
12+
path: z.string().optional(),
13+
/** Git URL for the dependency */
14+
git: z.string().optional(),
15+
/** Semver version */
16+
version: z.string().optional(),
17+
/** Name of a registry whose URL is configured in `.cargo/config.toml` or `.cargo/config` */
18+
registry: z.string().optional(),
19+
/** Name of a package to look up */
20+
package: z.string().optional(),
21+
/** Whether the dependency is inherited from the workspace */
22+
workspace: z.boolean().optional(),
23+
})
24+
.transform(
25+
({
26+
path,
27+
git,
28+
version,
29+
registry,
30+
package: pkg,
31+
workspace,
32+
}): PackageDependency<CargoManagerData> => {
33+
let skipReason: SkipReason | undefined;
34+
let currentValue: string | undefined;
35+
let nestedVersion = false;
36+
37+
if (version) {
38+
currentValue = version;
39+
nestedVersion = true;
40+
} else {
41+
currentValue = '';
42+
skipReason = 'invalid-dependency-specification';
43+
}
44+
45+
if (path) {
46+
skipReason = 'path-dependency';
47+
} else if (git) {
48+
skipReason = 'git-dependency';
49+
} else if (workspace) {
50+
skipReason = 'inherited-dependency';
51+
}
52+
53+
const dep: PackageDependency<CargoManagerData> = {
54+
currentValue,
55+
managerData: { nestedVersion },
56+
datasource: CrateDatasource.id,
57+
};
58+
59+
if (skipReason) {
60+
dep.skipReason = skipReason;
61+
}
62+
if (pkg) {
63+
dep.packageName = pkg;
64+
}
65+
if (registry) {
66+
dep.managerData!.registryName = registry;
67+
}
68+
69+
return dep;
70+
},
71+
),
72+
z.string().transform(
73+
(version): PackageDependency<CargoManagerData> => ({
74+
currentValue: version,
75+
managerData: { nestedVersion: false },
76+
datasource: CrateDatasource.id,
77+
}),
78+
),
79+
]);
80+
81+
const CargoDeps = z.record(z.string(), CargoDep).transform((record) => {
82+
const deps: PackageDependency[] = [];
83+
84+
for (const [depName, dep] of Object.entries(record)) {
85+
dep.depName = depName;
86+
deps.push(dep);
87+
}
88+
89+
return deps;
90+
});
91+
92+
export type CargoDeps = z.infer<typeof CargoDeps>;
93+
94+
const CargoSection = z.object({
95+
dependencies: withDepType(CargoDeps, 'dependencies').optional(),
96+
'dev-dependencies': withDepType(CargoDeps, 'dev-dependencies').optional(),
97+
'build-dependencies': withDepType(CargoDeps, 'build-dependencies').optional(),
98+
});
99+
100+
const CargoWorkspace = z.object({
101+
dependencies: withDepType(CargoDeps, 'workspace.dependencies').optional(),
102+
});
103+
104+
const CargoTarget = z.record(z.string(), CargoSection);
105+
106+
export const CargoManifestSchema = Toml.pipe(
107+
CargoSection.extend({
108+
package: z.object({ version: z.string().optional() }).optional(),
109+
workspace: CargoWorkspace.optional(),
110+
target: CargoTarget.optional(),
111+
}),
112+
);
113+
114+
const CargoConfigRegistry = z.object({
115+
index: z.string().optional(),
116+
});
117+
118+
const CargoConfigSource = z.object({
119+
'replace-with': z.string().optional(),
120+
registry: z.string().optional(),
121+
});
122+
123+
export const CargoConfigSchema = Toml.pipe(
124+
z.object({
125+
registries: z.record(z.string(), CargoConfigRegistry).optional(),
126+
source: z.record(z.string(), CargoConfigSource).optional(),
127+
}),
128+
);
129+
130+
export type CargoConfig = z.infer<typeof CargoConfigSchema>;
3131

4132
const CargoLockPackageSchema = z.object({
5133
name: z.string(),

‎lib/modules/manager/cargo/types.ts

+5-48
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,14 @@
11
import type { DEFAULT_REGISTRY_URL } from './utils';
22

3-
export interface CargoPackage {
4-
/** Semver version */
5-
version: string;
6-
}
7-
8-
export interface CargoDep {
9-
/** Path on disk to the crate sources */
10-
path?: string;
11-
/** Git URL for the dependency */
12-
git?: string;
13-
/** Semver version */
14-
version?: string;
15-
/** Name of a registry whose URL is configured in `.cargo/config.toml` */
16-
registry?: string;
17-
/** Name of a package to look up */
18-
package?: string;
19-
/** Whether the dependency is inherited from the workspace*/
20-
workspace?: boolean;
21-
}
22-
23-
export type CargoDeps = Record<string, CargoDep | string>;
24-
25-
export interface CargoSection {
26-
dependencies?: CargoDeps;
27-
'dev-dependencies'?: CargoDeps;
28-
'build-dependencies'?: CargoDeps;
29-
}
30-
31-
export interface CargoManifest extends CargoSection {
32-
target?: Record<string, CargoSection>;
33-
workspace?: CargoSection;
34-
package?: CargoPackage;
35-
}
36-
37-
export interface CargoConfig {
38-
registries?: Record<string, CargoRegistry>;
39-
source?: Record<string, CargoSource>;
40-
}
41-
42-
export interface CargoRegistry {
43-
index?: string;
44-
}
45-
46-
export interface CargoSource {
47-
'replace-with'?: string;
48-
registry?: string;
49-
}
50-
513
/**
524
* null means a registry was defined, but we couldn't find a valid URL
535
*/
546
export type CargoRegistryUrl = string | typeof DEFAULT_REGISTRY_URL | null;
557
export interface CargoRegistries {
568
[key: string]: CargoRegistryUrl;
579
}
10+
11+
export interface CargoManagerData {
12+
nestedVersion?: boolean;
13+
registryName?: string;
14+
}

‎lib/modules/manager/poetry/schema.ts

+6-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import type { ZodEffects, ZodType, ZodTypeDef } from 'zod';
21
import { z } from 'zod';
32
import { logger } from '../../../logger';
43
import { parseGitUrl } from '../../../util/git/url';
54
import { regEx } from '../../../util/regex';
6-
import { LooseArray, LooseRecord, Toml } from '../../../util/schema-utils';
5+
import {
6+
LooseArray,
7+
LooseRecord,
8+
Toml,
9+
withDepType,
10+
} from '../../../util/schema-utils';
711
import { uniq } from '../../../util/uniq';
812
import { GitRefsDatasource } from '../../datasource/git-refs';
913
import { GitTagsDatasource } from '../../datasource/git-tags';
@@ -172,18 +176,6 @@ export const PoetryDependencies = LooseRecord(
172176
return deps;
173177
});
174178

175-
function withDepType<
176-
Output extends PackageDependency[],
177-
Schema extends ZodType<Output, ZodTypeDef, unknown>,
178-
>(schema: Schema, depType: string): ZodEffects<Schema> {
179-
return schema.transform((deps) => {
180-
for (const dep of deps) {
181-
dep.depType = depType;
182-
}
183-
return deps;
184-
});
185-
}
186-
187179
export const PoetryGroupDependencies = LooseRecord(
188180
z.string(),
189181
z

‎lib/util/schema-utils.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import JSON5 from 'json5';
22
import * as JSONC from 'jsonc-parser';
33
import { DateTime } from 'luxon';
44
import type { JsonArray, JsonValue } from 'type-fest';
5-
import { z } from 'zod';
5+
import { type ZodEffects, type ZodType, type ZodTypeDef, z } from 'zod';
6+
import type { PackageDependency } from '../modules/manager/types';
67
import { parse as parseToml } from './toml';
78
import { parseSingleYaml, parseYaml } from './yaml';
89

@@ -263,3 +264,15 @@ export const Toml = z.string().transform((str, ctx) => {
263264
return z.NEVER;
264265
}
265266
});
267+
268+
export function withDepType<
269+
Output extends PackageDependency[],
270+
Schema extends ZodType<Output, ZodTypeDef, unknown>,
271+
>(schema: Schema, depType: string): ZodEffects<Schema> {
272+
return schema.transform((deps) => {
273+
for (const dep of deps) {
274+
dep.depType = depType;
275+
}
276+
return deps;
277+
});
278+
}

0 commit comments

Comments
 (0)
Please sign in to comment.