diff --git a/lib/modules/manager/cargo/__snapshots__/extract.spec.ts.snap b/lib/modules/manager/cargo/__snapshots__/extract.spec.ts.snap index 95a994670842be86eb567247ef3aa3526d0404d1..70efe56af7a70c3de51a96344ab5e4c6258bdade 100644 --- a/lib/modules/manager/cargo/__snapshots__/extract.spec.ts.snap +++ b/lib/modules/manager/cargo/__snapshots__/extract.spec.ts.snap @@ -461,6 +461,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() extracts registry ur "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "private-crates", }, "registryUrls": [ "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 "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "mcorbin", }, "registryUrls": [ "https://github.com/mcorbin/testregistry", @@ -499,6 +501,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() extracts registry ur "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "private-crates", }, "registryUrls": [ "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 "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "mcorbin", }, "registryUrls": [ "https://github.com/mcorbin/testregistry", @@ -537,6 +541,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() fails to parse cargo "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "private-crates", }, "skipReason": "unknown-registry", }, @@ -547,6 +552,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() fails to parse cargo "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "mcorbin", }, "skipReason": "unknown-registry", }, @@ -718,6 +724,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() ignore cargo config "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "private-crates", }, "skipReason": "unknown-registry", }, @@ -728,6 +735,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() ignore cargo config "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "mcorbin", }, "skipReason": "unknown-registry", }, @@ -752,6 +760,7 @@ exports[`modules/manager/cargo/extract extractPackageFile() skips unknown regist "depType": "dependencies", "managerData": { "nestedVersion": true, + "registryName": "not-listed", }, "skipReason": "unknown-registry", }, diff --git a/lib/modules/manager/cargo/extract.spec.ts b/lib/modules/manager/cargo/extract.spec.ts index 16d8c7b2b04ff2963f4eff2d297bb95568e2d299..6b48dc84378ea563fa55a5d3134dbff78366d002 100644 --- a/lib/modules/manager/cargo/extract.spec.ts +++ b/lib/modules/manager/cargo/extract.spec.ts @@ -136,6 +136,7 @@ replace-with = "private-crates"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'private-crates', }, registryUrls: [ 'https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git', @@ -148,6 +149,7 @@ replace-with = "private-crates"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'mcorbin', }, registryUrls: [ 'https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git', @@ -212,6 +214,7 @@ replace-with = "mcorbin"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'private-crates', }, }, { @@ -221,6 +224,7 @@ replace-with = "mcorbin"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'mcorbin', }, }, { @@ -302,6 +306,7 @@ replace-with = "mcorbin"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'private-crates', }, registryUrls: [ 'https://dl.cloudsmith.io/basic/my-org/my-repo/cargo/index.git', @@ -314,6 +319,7 @@ replace-with = "mcorbin"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'mcorbin', }, registryUrls: ['https://github.com/mcorbin/testregistry'], }, @@ -437,6 +443,7 @@ replace-with = "mine"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'private-crates', }, skipReason: 'unknown-registry', }, @@ -447,6 +454,7 @@ replace-with = "mine"`, depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'mcorbin', }, skipReason: 'unknown-registry', }, @@ -493,6 +501,7 @@ replace-with = "mcorbin" depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'private-crates', }, skipReason: 'unknown-registry', }, @@ -503,6 +512,7 @@ replace-with = "mcorbin" depType: 'dependencies', managerData: { nestedVersion: true, + registryName: 'mcorbin', }, skipReason: 'unknown-registry', }, diff --git a/lib/modules/manager/cargo/extract.ts b/lib/modules/manager/cargo/extract.ts index 76ea1bdd2de440d7bc0aafd4bd8837901a067767..f71df1f2226c9d9524bd9d4e9e8b8b4d49a169f2 100644 --- a/lib/modules/manager/cargo/extract.ts +++ b/lib/modules/manager/cargo/extract.ts @@ -1,9 +1,6 @@ import { logger } from '../../../logger'; -import type { SkipReason } from '../../../types'; import { coerceArray } from '../../../util/array'; import { findLocalSiblingOrParent, readLocalFile } from '../../../util/fs'; -import { parse as parseToml } from '../../../util/toml'; -import { CrateDatasource } from '../../datasource/crate'; import { api as versioning } from '../../versioning/cargo'; import type { ExtractConfig, @@ -11,12 +8,15 @@ import type { PackageFileContent, } from '../types'; import { extractLockFileVersions } from './locked-version'; +import { + type CargoConfig, + CargoConfigSchema, + CargoManifestSchema, +} from './schema'; import type { - CargoConfig, - CargoManifest, + CargoManagerData, CargoRegistries, CargoRegistryUrl, - CargoSection, } from './types'; import { DEFAULT_REGISTRY_URL } from './utils'; @@ -28,75 +28,32 @@ function getCargoIndexEnv(registryName: string): string | null { } function extractFromSection( - parsedContent: CargoSection, - section: keyof CargoSection, + dependencies: PackageDependency<CargoManagerData>[] | undefined, cargoRegistries: CargoRegistries, target?: string, - depTypeOverride?: string, ): PackageDependency[] { - const deps: PackageDependency[] = []; - const sectionContent = parsedContent[section]; - if (!sectionContent) { + if (!dependencies) { return []; } - Object.keys(sectionContent).forEach((depName) => { - let skipReason: SkipReason | undefined; - let currentValue = sectionContent[depName]; - let nestedVersion = false; - let registryUrls: string[] | undefined; - let packageName: string | undefined; - - if (typeof currentValue !== 'string') { - const version = currentValue.version; - const path = currentValue.path; - const git = currentValue.git; - const registryName = currentValue.registry; - const workspace = currentValue.workspace; - packageName = currentValue.package; + const deps: PackageDependency<CargoManagerData>[] = []; - if (version) { - currentValue = version; - nestedVersion = true; - if (registryName) { - const registryUrl = - getCargoIndexEnv(registryName) ?? cargoRegistries[registryName]; + for (const dep of Object.values(dependencies)) { + let registryUrls: string[] | undefined; - if (registryUrl) { - if (registryUrl !== DEFAULT_REGISTRY_URL) { - registryUrls = [registryUrl]; - } - } else { - skipReason = 'unknown-registry'; - } - } - if (path) { - skipReason = 'path-dependency'; - } - if (git) { - skipReason = 'git-dependency'; + if (dep.managerData?.registryName) { + const registryUrl = + getCargoIndexEnv(dep.managerData.registryName) ?? + cargoRegistries[dep.managerData?.registryName]; + if (registryUrl) { + if (registryUrl !== DEFAULT_REGISTRY_URL) { + registryUrls = [registryUrl]; } - } else if (path) { - currentValue = ''; - skipReason = 'path-dependency'; - } else if (git) { - currentValue = ''; - skipReason = 'git-dependency'; - } else if (workspace) { - currentValue = ''; - skipReason = 'inherited-dependency'; } else { - currentValue = ''; - skipReason = 'invalid-dependency-specification'; + dep.skipReason = 'unknown-registry'; } } - const dep: PackageDependency = { - depName, - depType: section, - currentValue: currentValue as any, - managerData: { nestedVersion }, - datasource: CrateDatasource.id, - }; + if (registryUrls) { dep.registryUrls = registryUrls; } else { @@ -108,24 +65,16 @@ function extractFromSection( } else { // we always expect to have DEFAULT_REGISTRY_ID set, if it's not it means the config defines an alternative // registry that we couldn't resolve. - skipReason = 'unknown-registry'; + dep.skipReason = 'unknown-registry'; } } - if (skipReason) { - dep.skipReason = skipReason; - } if (target) { dep.target = target; } - if (packageName) { - dep.packageName = packageName; - } - if (depTypeOverride) { - dep.depType = depTypeOverride; - } deps.push(dep); - }); + } + return deps; } @@ -135,12 +84,15 @@ async function readCargoConfig(): Promise<CargoConfig | null> { const path = `.cargo/${configName}`; const payload = await readLocalFile(path, 'utf8'); if (payload) { - try { - return parseToml(payload) as CargoConfig; - } catch (err) { - logger.debug({ err }, `Error parsing ${path}`); + const parsedCargoConfig = CargoConfigSchema.safeParse(payload); + if (parsedCargoConfig.success) { + return parsedCargoConfig.data; + } else { + logger.debug( + { err: parsedCargoConfig.error, path }, + `Error parsing cargo config`, + ); } - break; } } @@ -217,19 +169,23 @@ export async function extractPackageFile( content: string, packageFile: string, _config?: ExtractConfig, -): Promise<PackageFileContent | null> { +): Promise<PackageFileContent<CargoManagerData> | null> { logger.trace(`cargo.extractPackageFile(${packageFile})`); const cargoConfig = (await readCargoConfig()) ?? {}; const cargoRegistries = extractCargoRegistries(cargoConfig); - let cargoManifest: CargoManifest; - try { - cargoManifest = parseToml(content) as CargoManifest; - } catch (err) { - logger.debug({ err, packageFile }, 'Error parsing Cargo.toml file'); + const parsedCargoManifest = CargoManifestSchema.safeParse(content); + if (!parsedCargoManifest.success) { + logger.debug( + { err: parsedCargoManifest.error, packageFile }, + 'Error parsing Cargo.toml file', + ); return null; } + + const cargoManifest = parsedCargoManifest.data; + /* There are the following sections in Cargo.toml: [package] @@ -249,20 +205,17 @@ export async function extractPackageFile( // Dependencies for `${target}` const deps = [ ...extractFromSection( - targetContent, - 'dependencies', + targetContent.dependencies, cargoRegistries, target, ), ...extractFromSection( - targetContent, - 'dev-dependencies', + targetContent['dev-dependencies'], cargoRegistries, target, ), ...extractFromSection( - targetContent, - 'build-dependencies', + targetContent['build-dependencies'], cargoRegistries, target, ), @@ -275,18 +228,16 @@ export async function extractPackageFile( let workspaceDeps: PackageDependency[] = []; if (workspaceSection) { workspaceDeps = extractFromSection( - workspaceSection, - 'dependencies', + workspaceSection.dependencies, cargoRegistries, undefined, - 'workspace.dependencies', ); } const deps = [ - ...extractFromSection(cargoManifest, 'dependencies', cargoRegistries), - ...extractFromSection(cargoManifest, 'dev-dependencies', cargoRegistries), - ...extractFromSection(cargoManifest, 'build-dependencies', cargoRegistries), + ...extractFromSection(cargoManifest.dependencies, cargoRegistries), + ...extractFromSection(cargoManifest['dev-dependencies'], cargoRegistries), + ...extractFromSection(cargoManifest['build-dependencies'], cargoRegistries), ...targetDeps, ...workspaceDeps, ]; diff --git a/lib/modules/manager/cargo/schema.ts b/lib/modules/manager/cargo/schema.ts index 4e73235f60cf33f5bebdf14ec628025167a2b083..52640d4252d8937492c26353962c8f986ea12bec 100644 --- a/lib/modules/manager/cargo/schema.ts +++ b/lib/modules/manager/cargo/schema.ts @@ -1,5 +1,133 @@ import { z } from 'zod'; -import { Toml } from '../../../util/schema-utils'; +import type { SkipReason } from '../../../types'; +import { Toml, withDepType } from '../../../util/schema-utils'; +import { CrateDatasource } from '../../datasource/crate'; +import type { PackageDependency } from '../types'; +import type { CargoManagerData } from './types'; + +const CargoDep = z.union([ + z + .object({ + /** Path on disk to the crate sources */ + path: z.string().optional(), + /** Git URL for the dependency */ + git: z.string().optional(), + /** Semver version */ + version: z.string().optional(), + /** Name of a registry whose URL is configured in `.cargo/config.toml` or `.cargo/config` */ + registry: z.string().optional(), + /** Name of a package to look up */ + package: z.string().optional(), + /** Whether the dependency is inherited from the workspace */ + workspace: z.boolean().optional(), + }) + .transform( + ({ + path, + git, + version, + registry, + package: pkg, + workspace, + }): PackageDependency<CargoManagerData> => { + let skipReason: SkipReason | undefined; + let currentValue: string | undefined; + let nestedVersion = false; + + if (version) { + currentValue = version; + nestedVersion = true; + } else { + currentValue = ''; + skipReason = 'invalid-dependency-specification'; + } + + if (path) { + skipReason = 'path-dependency'; + } else if (git) { + skipReason = 'git-dependency'; + } else if (workspace) { + skipReason = 'inherited-dependency'; + } + + const dep: PackageDependency<CargoManagerData> = { + currentValue, + managerData: { nestedVersion }, + datasource: CrateDatasource.id, + }; + + if (skipReason) { + dep.skipReason = skipReason; + } + if (pkg) { + dep.packageName = pkg; + } + if (registry) { + dep.managerData!.registryName = registry; + } + + return dep; + }, + ), + z.string().transform( + (version): PackageDependency<CargoManagerData> => ({ + currentValue: version, + managerData: { nestedVersion: false }, + datasource: CrateDatasource.id, + }), + ), +]); + +const CargoDeps = z.record(z.string(), CargoDep).transform((record) => { + const deps: PackageDependency[] = []; + + for (const [depName, dep] of Object.entries(record)) { + dep.depName = depName; + deps.push(dep); + } + + return deps; +}); + +export type CargoDeps = z.infer<typeof CargoDeps>; + +const CargoSection = z.object({ + dependencies: withDepType(CargoDeps, 'dependencies').optional(), + 'dev-dependencies': withDepType(CargoDeps, 'dev-dependencies').optional(), + 'build-dependencies': withDepType(CargoDeps, 'build-dependencies').optional(), +}); + +const CargoWorkspace = z.object({ + dependencies: withDepType(CargoDeps, 'workspace.dependencies').optional(), +}); + +const CargoTarget = z.record(z.string(), CargoSection); + +export const CargoManifestSchema = Toml.pipe( + CargoSection.extend({ + package: z.object({ version: z.string().optional() }).optional(), + workspace: CargoWorkspace.optional(), + target: CargoTarget.optional(), + }), +); + +const CargoConfigRegistry = z.object({ + index: z.string().optional(), +}); + +const CargoConfigSource = z.object({ + 'replace-with': z.string().optional(), + registry: z.string().optional(), +}); + +export const CargoConfigSchema = Toml.pipe( + z.object({ + registries: z.record(z.string(), CargoConfigRegistry).optional(), + source: z.record(z.string(), CargoConfigSource).optional(), + }), +); + +export type CargoConfig = z.infer<typeof CargoConfigSchema>; const CargoLockPackageSchema = z.object({ name: z.string(), diff --git a/lib/modules/manager/cargo/types.ts b/lib/modules/manager/cargo/types.ts index b28c3ced50fa27c1684053a17286b8d8e923df9d..d2e3f940493373b8b6d0a7357245c253d969049f 100644 --- a/lib/modules/manager/cargo/types.ts +++ b/lib/modules/manager/cargo/types.ts @@ -1,53 +1,5 @@ import type { DEFAULT_REGISTRY_URL } from './utils'; -export interface CargoPackage { - /** Semver version */ - version: string; -} - -export interface CargoDep { - /** Path on disk to the crate sources */ - path?: string; - /** Git URL for the dependency */ - git?: string; - /** Semver version */ - version?: string; - /** Name of a registry whose URL is configured in `.cargo/config.toml` */ - registry?: string; - /** Name of a package to look up */ - package?: string; - /** Whether the dependency is inherited from the workspace*/ - workspace?: boolean; -} - -export type CargoDeps = Record<string, CargoDep | string>; - -export interface CargoSection { - dependencies?: CargoDeps; - 'dev-dependencies'?: CargoDeps; - 'build-dependencies'?: CargoDeps; -} - -export interface CargoManifest extends CargoSection { - target?: Record<string, CargoSection>; - workspace?: CargoSection; - package?: CargoPackage; -} - -export interface CargoConfig { - registries?: Record<string, CargoRegistry>; - source?: Record<string, CargoSource>; -} - -export interface CargoRegistry { - index?: string; -} - -export interface CargoSource { - 'replace-with'?: string; - registry?: string; -} - /** * null means a registry was defined, but we couldn't find a valid URL */ @@ -55,3 +7,8 @@ export type CargoRegistryUrl = string | typeof DEFAULT_REGISTRY_URL | null; export interface CargoRegistries { [key: string]: CargoRegistryUrl; } + +export interface CargoManagerData { + nestedVersion?: boolean; + registryName?: string; +} diff --git a/lib/modules/manager/poetry/schema.ts b/lib/modules/manager/poetry/schema.ts index ac5c7e741c293a2f397cd025add785dae98afabb..25735dc3ff865482294d380bcbdf827aceaf67c6 100644 --- a/lib/modules/manager/poetry/schema.ts +++ b/lib/modules/manager/poetry/schema.ts @@ -1,9 +1,13 @@ -import type { ZodEffects, ZodType, ZodTypeDef } from 'zod'; import { z } from 'zod'; import { logger } from '../../../logger'; import { parseGitUrl } from '../../../util/git/url'; import { regEx } from '../../../util/regex'; -import { LooseArray, LooseRecord, Toml } from '../../../util/schema-utils'; +import { + LooseArray, + LooseRecord, + Toml, + withDepType, +} from '../../../util/schema-utils'; import { uniq } from '../../../util/uniq'; import { GitRefsDatasource } from '../../datasource/git-refs'; import { GitTagsDatasource } from '../../datasource/git-tags'; @@ -172,18 +176,6 @@ export const PoetryDependencies = LooseRecord( return deps; }); -function withDepType< - Output extends PackageDependency[], - Schema extends ZodType<Output, ZodTypeDef, unknown>, ->(schema: Schema, depType: string): ZodEffects<Schema> { - return schema.transform((deps) => { - for (const dep of deps) { - dep.depType = depType; - } - return deps; - }); -} - export const PoetryGroupDependencies = LooseRecord( z.string(), z diff --git a/lib/util/schema-utils.ts b/lib/util/schema-utils.ts index 4a1c35d98d6d3f73b372d232a7a8b37e001c8a8d..f5a12c54095685ce48bd2ae74dedd1c90141c8f6 100644 --- a/lib/util/schema-utils.ts +++ b/lib/util/schema-utils.ts @@ -2,7 +2,8 @@ import JSON5 from 'json5'; import * as JSONC from 'jsonc-parser'; import { DateTime } from 'luxon'; import type { JsonArray, JsonValue } from 'type-fest'; -import { z } from 'zod'; +import { type ZodEffects, type ZodType, type ZodTypeDef, z } from 'zod'; +import type { PackageDependency } from '../modules/manager/types'; import { parse as parseToml } from './toml'; import { parseSingleYaml, parseYaml } from './yaml'; @@ -263,3 +264,15 @@ export const Toml = z.string().transform((str, ctx) => { return z.NEVER; } }); + +export function withDepType< + Output extends PackageDependency[], + Schema extends ZodType<Output, ZodTypeDef, unknown>, +>(schema: Schema, depType: string): ZodEffects<Schema> { + return schema.transform((deps) => { + for (const dep of deps) { + dep.depType = depType; + } + return deps; + }); +}