mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
23272951d6
* refactor(manager/gradle): add helper for matching `group:artifact` strings As part of follow-up changes, we'll extract dependencies whose version is declared separately from their coordinates, so we need a way to validate a bare `group:artifact` notation. `isDependencyString` can't be used, as it requires a version to be present. This isn't currently used. Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io> * feat(manager/gradle): support rich version constraints in the Groovy/Kotlin DSL As noted in discussion #45481, Renovate doesn't extract dependencies whose version comes from a Gradle rich version constraint[0], so a `strictly` pin never receives updates - not even for a security fix: ```groovy dependencies { implementation('org.slf4j:slf4j-api') { version { strictly '[1.7, 1.8[' } } } ``` The version catalog extractor already handles rich versions, so the missing piece is the `version { ... }` block in the Groovy and Kotlin DSLs. Renovate can only rewrite a single version literal per dependency, so we pick one constraint to update and record which one in `managerData.versionConstraint`: - `strictly` -> bump `strictly` - `strictly` (single version) + `prefer` -> bump `strictly`, as `prefer` is inert when there's nothing to choose between - `require`, optionally with `prefer` -> bump `require`, as `prefer` is only a hint and may not be what gets resolved - `prefer` alone -> bump `prefer` - `require` + `strictly` -> skip as `multiple-constraint-dep`, as Gradle applies `require()` before `strictly()`, so bumping only `strictly` would leave the `require` literal behind as stale text - `strictly` range + `prefer` -> skip as `multiple-constraint-dep`, as `prefer` has to keep pointing inside the range and we can't move both - anything with `reject`/`rejectAll` -> skip as `unsupported-version`, matching what the version catalog extractor already does As `strictly` and `prefer` pin a version on purpose, updating them by default would be surprising, so those deps are extracted with `enabled: false` - meaning they only receive updates for vulnerability alerts, which force through. Users who want regular updates can opt in with a package rule matching `managerData.versionConstraint`, similar to how `manager/gomod` handles `depType: indirect`. A `require` constraint means the same thing as a plain version declaration, so it is updated like any other dependency. This doesn't unify behaviour with version catalogs, which extract a single `require`/`prefer`/`strictly` and update it without disabling it. Aligning them would take away updates users get today, so that's left as a separate decision. The `!!` shorthand (i.e. `foo:bar:1.7.25!!`) is untouched - it already extracts as an opaque version string. Rich versions declared in the Groovy/Kotlin `versionCatalogs { }` builder, as opposed to TOML, are still dropped silently. [0]: https://docs.gradle.org/current/userguide/dependency_versions.html#sec:rich-version-constraints Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io> * fix(vulnerabilities): raise fixes for deps their manager disables by default * test(workers/fetch): set packageName on disabled-dep fixtures * test(manager/gradle): assert enabled is not set --------- Co-authored-by: Jamie Tanna <jamie.tanna@mend.io> Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
248 lines
6.6 KiB
TypeScript
248 lines
6.6 KiB
TypeScript
import upath from 'upath';
|
|
import { coerceObject } from '../../../util/object.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { api as gradleVersioning } from '../../versioning/gradle/index.ts';
|
|
import type { PackageDependency } from '../types.ts';
|
|
import type {
|
|
GradleManagerData,
|
|
PackageVariables,
|
|
VariableRegistry,
|
|
} from './types.ts';
|
|
|
|
const artifactRegex = regEx(
|
|
'^[a-zA-Z][-_a-zA-Z0-9]*(?:\\.[a-zA-Z0-9][-_a-zA-Z0-9]*?)*$',
|
|
);
|
|
|
|
const versionLikeRegex = regEx('^(?<version>[-_.\\[\\](),a-zA-Z0-9+! ]+)');
|
|
|
|
// Extracts version-like and range-like strings from the beginning of input
|
|
export function versionLikeSubstring(
|
|
input: string | null | undefined,
|
|
): string | null {
|
|
if (!input) {
|
|
return null;
|
|
}
|
|
|
|
const match = versionLikeRegex.exec(input);
|
|
const version = match?.groups?.version?.trim();
|
|
if (!version || !regEx(/\d/).test(version)) {
|
|
return null;
|
|
}
|
|
|
|
if (!gradleVersioning.isValid(version)) {
|
|
return null;
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
export function isDependencyString(input: string): boolean {
|
|
const [depNotation, ...extra] = input.split('@');
|
|
if (extra.length > 1) {
|
|
return false;
|
|
}
|
|
|
|
const parts = depNotation.split(':');
|
|
if (parts.length !== 3 && parts.length !== 4) {
|
|
return false;
|
|
}
|
|
|
|
const [groupId, artifactId, version, classifier] = parts;
|
|
|
|
return !!(
|
|
groupId &&
|
|
artifactId &&
|
|
version &&
|
|
artifactRegex.test(groupId) &&
|
|
artifactRegex.test(artifactId) &&
|
|
(!classifier || artifactRegex.test(classifier)) &&
|
|
version === versionLikeSubstring(version)
|
|
);
|
|
}
|
|
|
|
// Matches the `group:artifact` notation used by dependencies whose version is
|
|
// declared separately, e.g. inside a rich version constraint block
|
|
export function isGroupArtifactString(input: string): boolean {
|
|
const parts = input.split(':');
|
|
if (parts.length !== 2) {
|
|
return false;
|
|
}
|
|
|
|
const [groupId, artifactId] = parts;
|
|
|
|
return !!(
|
|
groupId &&
|
|
artifactId &&
|
|
artifactRegex.test(groupId) &&
|
|
artifactRegex.test(artifactId)
|
|
);
|
|
}
|
|
|
|
export function parseDependencyString(
|
|
input: string,
|
|
): PackageDependency<GradleManagerData> | null {
|
|
if (!isDependencyString(input)) {
|
|
return null;
|
|
}
|
|
|
|
const [depNotation, dataType] = input.split('@');
|
|
const [groupId, artifactId, currentValue] = depNotation.split(':');
|
|
|
|
return {
|
|
depName: `${groupId}:${artifactId}`,
|
|
currentValue,
|
|
...(dataType && { dataType }),
|
|
};
|
|
}
|
|
|
|
const gradleVersionsFileRegex = regEx('^versions\\.gradle(?:\\.kts)?$', 'i');
|
|
const gradleBuildFileRegex = regEx('^build\\.gradle(?:\\.kts)?$', 'i');
|
|
const gradleSettingsFileRegex = regEx('^settings\\.gradle(?:\\.kts)?$', 'i');
|
|
|
|
export function isGradleScriptFile(path: string): boolean {
|
|
const filename = upath.basename(path).toLowerCase();
|
|
return filename.endsWith('.gradle.kts') || filename.endsWith('.gradle');
|
|
}
|
|
|
|
export function isGradleVersionsFile(path: string): boolean {
|
|
const filename = upath.basename(path);
|
|
return gradleVersionsFileRegex.test(filename);
|
|
}
|
|
|
|
export function isGradleBuildFile(path: string): boolean {
|
|
const filename = upath.basename(path);
|
|
return gradleBuildFileRegex.test(filename);
|
|
}
|
|
|
|
export function isGradleSettingsFile(path: string): boolean {
|
|
const filename = upath.basename(path);
|
|
return gradleSettingsFileRegex.test(filename);
|
|
}
|
|
|
|
export function isGradleDefaultCatalogFile(path: string): boolean {
|
|
return path.endsWith('/gradle/libs.versions.toml');
|
|
}
|
|
|
|
export function isPropsFile(path: string): boolean {
|
|
const filename = upath.basename(path).toLowerCase();
|
|
return filename === 'gradle.properties';
|
|
}
|
|
|
|
export function isKotlinSourceFile(path: string): boolean {
|
|
const filename = upath.basename(path).toLowerCase();
|
|
return filename.endsWith('.kt');
|
|
}
|
|
|
|
export function isTOMLFile(path: string): boolean {
|
|
const filename = upath.basename(path).toLowerCase();
|
|
return filename.endsWith('.toml');
|
|
}
|
|
|
|
export function toAbsolutePath(packageFile: string): string {
|
|
return upath.join(packageFile.replace(regEx(/^[/\\]*/), '/'));
|
|
}
|
|
|
|
function getFileRank(filename: string): number {
|
|
if (isPropsFile(filename)) {
|
|
return 0;
|
|
}
|
|
if (isGradleSettingsFile(filename)) {
|
|
return 1;
|
|
}
|
|
if (isGradleDefaultCatalogFile(filename)) {
|
|
return 2;
|
|
}
|
|
if (isGradleVersionsFile(filename)) {
|
|
return 3;
|
|
}
|
|
if (isGradleBuildFile(filename)) {
|
|
return 5;
|
|
}
|
|
return 4;
|
|
}
|
|
|
|
export function reorderFiles(packageFiles: string[]): string[] {
|
|
return packageFiles
|
|
.map((path) => {
|
|
const absPath = toAbsolutePath(path);
|
|
const currentDir = upath.dirname(absPath);
|
|
|
|
return {
|
|
path,
|
|
absPath,
|
|
dir: isGradleDefaultCatalogFile(absPath)
|
|
? upath.dirname(currentDir)
|
|
: currentDir,
|
|
rank: getFileRank(absPath),
|
|
};
|
|
})
|
|
.sort((a, b) => {
|
|
// Different directories: check parent-child relationship
|
|
if (a.dir !== b.dir) {
|
|
if (a.dir.startsWith(`${b.dir}/`)) {
|
|
return 1;
|
|
}
|
|
if (b.dir.startsWith(`${a.dir}/`)) {
|
|
return -1;
|
|
}
|
|
return a.dir.localeCompare(b.dir);
|
|
}
|
|
|
|
// Same effective directory: rank, if equal sort alphabetically
|
|
return a.rank - b.rank || a.absPath.localeCompare(b.absPath);
|
|
})
|
|
.map((entry) => entry.path);
|
|
}
|
|
|
|
export function getVars(
|
|
registry: VariableRegistry,
|
|
dir: string,
|
|
vars: PackageVariables = registry[dir] || {},
|
|
): PackageVariables {
|
|
const dirAbs = toAbsolutePath(dir);
|
|
const parentDir = upath.dirname(dirAbs);
|
|
if (parentDir === dirAbs) {
|
|
return vars;
|
|
}
|
|
const parentVars = registry[parentDir] || {};
|
|
return getVars(registry, parentDir, { ...parentVars, ...vars });
|
|
}
|
|
|
|
export function updateVars(
|
|
registry: VariableRegistry,
|
|
dir: string,
|
|
newVars: PackageVariables,
|
|
): void {
|
|
const oldVars = coerceObject(registry[dir]);
|
|
registry[dir] = { ...oldVars, ...newVars };
|
|
}
|
|
|
|
export function updateVarsFromDefaultCatalog(
|
|
registry: VariableRegistry,
|
|
dir: string,
|
|
packageFile: string,
|
|
newVars: PackageVariables,
|
|
): void {
|
|
if (!isGradleDefaultCatalogFile(toAbsolutePath(packageFile))) {
|
|
return;
|
|
}
|
|
|
|
const rootDir = upath.dirname(dir);
|
|
const oldVars = coerceObject(registry[rootDir]);
|
|
let defaultLibsExtName = 'libs';
|
|
if (
|
|
oldVars.defaultLibrariesExtensionName?.packageFile &&
|
|
isGradleSettingsFile(oldVars.defaultLibrariesExtensionName.packageFile)
|
|
) {
|
|
defaultLibsExtName = oldVars.defaultLibrariesExtensionName.value;
|
|
}
|
|
|
|
const newVarsRemapped: PackageVariables = {};
|
|
for (const [oldKey, variableData] of Object.entries(newVars)) {
|
|
const key = `${defaultLibsExtName}.versions.${oldKey}`;
|
|
newVarsRemapped[key] = { ...variableData, key };
|
|
}
|
|
|
|
registry[rootDir] = { ...oldVars, ...newVarsRemapped };
|
|
}
|