Files
renovate/lib/modules/manager/pipenv/extract.ts
Michael Kriese 9ac59d69d7 test(manager): mark remaining unreachable manager guards (#46082)
* test(manager): close helmfile, hermit, leiningen and mix gaps

Adds a leiningen vector whose version references a variable that was
never defined, which yields no dep.

Marks six guards. The mix host rules were already filtered on a match
host and on the same regex, hermit tracks its packages as symlinks, and
the remaining three need a repository the helm login helper cannot
handle, an umbrella parent lock file that reads cleanly, or a dep name
carrying no organization.

The helmfile repository chain stays counted: `repoName` is only left
null when an oci dep was set, so its implicit else cannot run, and a
coverage-ignore hint does not attach to an `else if`.

Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>

* test(manager): mark the nuget, ocb and pep621 guards

Twelve guards, none of which a test reaches. A global.json without an
sdk version is rejected a few lines earlier, and the nuget registry loop
always finds at least one url. The rest need inputs no fixture provides:
a blank ContainerBaseImage, a package reference pinned to a non-semver
value, a registry url with no protocol version, a host rule carrying
only a username or only a password, and a pixi manifest declaring its
requirement only under workspace.

The nuget lock file chain stays counted, since a coverage-ignore hint
does not attach to an `else if`.

Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>

* test(manager): mark the python and vendir guards

Ten guards across pip-compile, pipenv, pixi, poetry and vendir. A
compile command always records its arguments, every lock file of a
package file is already in the sorted list, a python dep always carries
a current value, a successful parse always yields a value, and the
vendir vendor dir is always set. The rest need credentials no fixture
supplies, a requirement naming an undeclared index, or a source url
outside artifact registry.

The travis node_js chain stays counted, since a coverage-ignore hint
does not attach to an `else if`.

Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io>

* test(manager): close the deps-edn, docker-compose and pep723 gaps

Three of the remaining manager branches turn out to be reachable after
all.

The deps-edn extractor skipped an `aliases` value that is not a map; the
existing test only covered an alias *entry* that is not a map, so the
outer guard was never taken. A modern docker-compose file with an `x-`
extension that carries no image never reached the schema's extension
filter either, because the only such fixture omitted `services` and so
parsed as a v1 file. A PEP 723 script block without `requires-python`
was likewise untested.

Co-Authored-By: Claude Opus 5 (1M context) <michael.kriese+claude-code@mend.io>

* test(manager/nuget): cover a global.json with no sdk version

The `v8 ignore else` on the sdk-version check claimed the guard above it
rejects any global.json without one. That guard is
`!manifest['msbuild-sdks'] && !manifest.sdk?.version`, so it rejects only
when both are missing — a file carrying just `msbuild-sdks` passes it and
takes the else.

Replace the ignore with a test for that shape, plus one for an empty
`{}` document, which the guard does reject.

Co-Authored-By: Claude Opus 5 (1M context) <michael.kriese+claude-code@mend.io>

---------

Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
2026-09-16 06:43:01 +00:00

187 lines
5.9 KiB
TypeScript

import { pipenv as pipenvDetect } from '@renovatebot/detect-tools';
import { RANGE_PATTERN } from '@renovatebot/pep440';
import { isArray, isObject, isString, isTruthy } from '@sindresorhus/is';
import { logger } from '../../../logger/index.ts';
import type { SkipReason } from '../../../types/index.ts';
import type { ConstraintName } from '../../../util/exec/types.ts';
import { getParentDir, localPathExists } from '../../../util/fs/index.ts';
import { ensureLocalPath } from '../../../util/fs/util.ts';
import { regEx } from '../../../util/regex.ts';
import { parse as parseToml } from '../../../util/toml.ts';
import { normalizePythonDepName } from '../../datasource/pypi/common.ts';
import { PypiDatasource } from '../../datasource/pypi/index.ts';
import type { PackageDependency, PackageFileContent } from '../types.ts';
import type { PipFile, PipRequirement, PipSource } from './types.ts';
// based on https://www.python.org/dev/peps/pep-0508/#names
export const packagePattern = '[A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9]';
export const extrasPattern = '(?:\\s*\\[[^\\]]+\\])*';
const packageRegex = regEx(`^(${packagePattern})(${extrasPattern})$`, 'i');
const rangePattern: string = RANGE_PATTERN;
const specifierPartPattern = `\\s*${rangePattern.replace(
regEx(/\?<\w+>/g),
'?:',
)}\\s*`;
const specifierPattern = `${specifierPartPattern}(?:,${specifierPartPattern})*`;
const specifierRegex = regEx(`^${specifierPattern}$`);
function extractFromSection(
sectionName: string,
pipfileSection: Record<string, PipRequirement>,
sources?: PipSource[],
): PackageDependency[] {
const deps = Object.entries(pipfileSection)
.map((x) => {
const [packageNameString, requirements] = x;
let depName = packageNameString;
let currentValue: string | undefined;
let nestedVersion = false;
let skipReason: SkipReason | undefined;
if (isObject(requirements)) {
if (requirements.git) {
skipReason = 'git-dependency';
} else if (requirements.file) {
skipReason = 'file-dependency';
} else if (requirements.path) {
skipReason = 'local-dependency';
} else if (requirements.version) {
currentValue = requirements.version;
nestedVersion = true;
} else {
skipReason = 'unspecified-version';
}
} else {
currentValue = requirements;
}
if (currentValue === '*') {
skipReason = 'unspecified-version';
}
if (!skipReason) {
const packageMatches = packageRegex.exec(packageNameString);
if (packageMatches) {
depName = packageMatches[1];
} else {
logger.debug(
`Skipping dependency with malformed package name "${packageNameString}".`,
);
skipReason = 'invalid-name';
}
// validated above
const specifierMatches = specifierRegex.exec(currentValue!);
if (!specifierMatches) {
logger.debug(
`Skipping dependency with malformed version specifier "${currentValue!}".`,
);
skipReason = 'invalid-version';
}
}
const dep: PackageDependency = {
depType: sectionName,
depName,
packageName: normalizePythonDepName(depName),
managerData: {},
};
if (currentValue) {
dep.currentValue = currentValue;
}
if (skipReason) {
dep.skipReason = skipReason;
} else {
dep.datasource = PypiDatasource.id;
}
if (!skipReason && currentValue?.startsWith('==')) {
dep.currentVersion = currentValue.replace(regEx(/^==\s*/), '');
}
if (nestedVersion) {
// TODO #22198
dep.managerData!.nestedVersion = nestedVersion;
}
if (sources && isObject(requirements) && requirements.index) {
const source = sources.find((item) => item.name === requirements.index);
// v8 ignore else -- needs a requirement naming an index that is not declared
if (source) {
dep.registryUrls = [source.url];
}
}
return dep;
})
.filter(isTruthy);
return deps;
}
function isPipRequirements(
section?:
Record<string, PipRequirement> | Record<string, string> | PipSource[],
): section is Record<string, PipRequirement> {
return (
!isArray(section) &&
isObject(section) &&
!Object.values(section).some((dep) => !isObject(dep) && !isString(dep))
);
}
export async function extractPackageFile(
content: string,
packageFile: string,
): Promise<PackageFileContent | null> {
logger.trace(`pipenv.extractPackageFile(${packageFile})`);
let pipfile: PipFile;
try {
// TODO: fix type (#9610)
pipfile = parseToml(content) as any;
} catch (err) {
logger.debug({ err, packageFile }, 'Error parsing Pipfile');
return null;
}
const res: PackageFileContent = { deps: [] };
const sources = pipfile?.source;
if (sources) {
res.registryUrls = sources.map((source) => source.url);
}
res.deps = Object.entries(pipfile)
.map(([category, section]) => {
if (
category === 'source' ||
category === 'requires' ||
!isPipRequirements(section)
) {
return [];
}
return extractFromSection(category, section, sources);
})
.flat();
if (!res.deps.length) {
return null;
}
const extractedConstraints: Partial<Record<ConstraintName, string>> = {};
const pipfileDir = getParentDir(ensureLocalPath(packageFile));
const pythonConstraint = await pipenvDetect.getPythonConstraint(pipfileDir);
if (pythonConstraint) {
extractedConstraints.python = pythonConstraint;
}
const pipenvConstraint = await pipenvDetect.getPipenvConstraint(pipfileDir);
if (pipenvConstraint) {
extractedConstraints.pipenv = pipenvConstraint;
}
const lockFileName = `${packageFile}.lock`;
if (await localPathExists(lockFileName)) {
res.lockFiles = [lockFileName];
}
res.extractedConstraints = extractedConstraints;
return res;
}