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

feat(gradle/manager): add support for Kotlin objects in buildSrc files #21892

Merged
merged 4 commits into from May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
120 changes: 120 additions & 0 deletions lib/modules/manager/gradle/extract.spec.ts
Expand Up @@ -120,6 +120,126 @@ describe('modules/manager/gradle/extract', () => {
]);
});

it('resolves cross-file Kotlin objects', async () => {
const fsMock = {
'buildSrc/src/main/kotlin/Deps.kt': codeBlock`
object Libraries {
const val jacksonAnnotations = "com.fasterxml.jackson.core:jackson-annotations:\${Versions.jackson}"
const val rxjava: String = "io.reactivex.rxjava2:rxjava:" + Versions.rxjava
const val jCache = "javax.cache:cache-api:1.1.0"
private const val shadowVersion = "7.1.2"

object Kotlin {
const val version = GradleDeps.Kotlin.version
const val stdlibJdk = "org.jetbrains.kotlin:kotlin-stdlib:$version"
}

object Android {
object Tools {
private const val version = "4.1.2"
const val buildGradle = "com.android.tools.build:gradle:$version"
}
}

val modulePlugins = mapOf(
"shadow" to shadowVersion
)

object Test {
private const val version = "1.3.0-rc01"
const val core = "androidx.test:core:\${Test.version}"

object Espresso {
private const val version = "3.3.0-rc01"
const val espressoCore = "androidx.test.espresso:espresso-core:$version"
}

object Androidx {
const val coreKtx = "androidx.test:core-ktx:$version"
}
}
}
`,
'buildSrc/src/main/kotlin/GradleDeps.kt': codeBlock`
object GradleDeps {
object Kotlin {
const val version = "1.8.10"
}
}
`,
'buildSrc/src/main/kotlin/Versions.kt': codeBlock`
object Versions {
const val jackson = "2.9.10"
const val rxjava: String = "1.2.3"
}
`,
};
mockFs(fsMock);

const res = await extractAllPackageFiles(
partial<ExtractConfig>(),
Object.keys(fsMock)
);

expect(res).toMatchObject([
{
packageFile: 'buildSrc/src/main/kotlin/Deps.kt',
deps: [
{
depName: 'javax.cache:cache-api',
currentValue: '1.1.0',
groupName: 'Libraries.jCache',
},
{
depName: 'com.android.tools.build:gradle',
currentValue: '4.1.2',
groupName: 'Libraries.Android.Tools.version',
},
{
depName: 'androidx.test:core',
currentValue: '1.3.0-rc01',
groupName: 'Libraries.Test.version',
},
{
depName: 'androidx.test.espresso:espresso-core',
currentValue: '3.3.0-rc01',
groupName: 'Libraries.Test.Espresso.version',
},
{
depName: 'androidx.test:core-ktx',
currentValue: '1.3.0-rc01',
groupName: 'Libraries.Test.version',
},
],
},
{
packageFile: 'buildSrc/src/main/kotlin/GradleDeps.kt',
deps: [
{
depName: 'org.jetbrains.kotlin:kotlin-stdlib',
currentValue: '1.8.10',
groupName: 'GradleDeps.Kotlin.version',
},
],
},
{
packageFile: 'buildSrc/src/main/kotlin/Versions.kt',
deps: [
{
depName: 'com.fasterxml.jackson.core:jackson-annotations',
currentValue: '2.9.10',
groupName: 'Versions.jackson',
},
{
depName: 'io.reactivex.rxjava2:rxjava',
currentValue: '1.2.3',
groupName: 'Versions.rxjava',
},
],
},
]);
});

it('inherits gradle variables', async () => {
const fsMock = {
'gradle.properties': 'foo=1.0.0',
Expand Down
26 changes: 20 additions & 6 deletions lib/modules/manager/gradle/extract.ts
Expand Up @@ -9,7 +9,7 @@ import {
parseGcv,
usesGcv,
} from './extract/consistent-versions-plugin';
import { parseGradle, parseProps } from './parser';
import { parseGradle, parseKotlinSource, parseProps } from './parser';
import { REGISTRY_URLS } from './parser/common';
import type {
GradleManagerData,
Expand All @@ -19,6 +19,7 @@ import type {
import {
getVars,
isGradleScriptFile,
isKotlinSourceFile,
isPropsFile,
isTOMLFile,
reorderFiles,
Expand Down Expand Up @@ -94,6 +95,15 @@ async function parsePackageFiles(
) {
const deps = parseGcv(packageFile, fileContents);
extractedDeps.push(...deps);
} else if (isKotlinSourceFile(packageFile)) {
const vars = getVars(varRegistry, packageFileDir);
const { vars: gradleVars, deps } = parseKotlinSource(
content,
vars,
packageFile
);
updateVars(varRegistry, '/', gradleVars);
extractedDeps.push(...deps);
} else if (isGradleScriptFile(packageFile)) {
const vars = getVars(varRegistry, packageFileDir);
const {
Expand Down Expand Up @@ -123,11 +133,14 @@ export async function extractAllPackageFiles(
const packageFilesByName: Record<string, PackageFile> = {};
const packageRegistries: PackageRegistry[] = [];
const extractedDeps: PackageDependency<GradleManagerData>[] = [];
const gradleFiles = reorderFiles(packageFiles);
const kotlinSourceFiles = packageFiles.filter(isKotlinSourceFile);
const gradleFiles = reorderFiles(
packageFiles.filter((e) => !kotlinSourceFiles.includes(e))
);

await parsePackageFiles(
config,
gradleFiles,
[...kotlinSourceFiles, ...kotlinSourceFiles, ...gradleFiles],
extractedDeps,
packageFilesByName,
packageRegistries
Expand Down Expand Up @@ -161,9 +174,10 @@ export async function extractAllPackageFiles(
dep.registryUrls = getRegistryUrlsForDep(packageRegistries, dep);

if (!dep.depType) {
dep.depType = key.startsWith('buildSrc')
? 'devDependencies'
: 'dependencies';
dep.depType =
key.startsWith('buildSrc') && !kotlinSourceFiles.length
? 'devDependencies'
: 'dependencies';
}
}

Expand Down
1 change: 1 addition & 0 deletions lib/modules/manager/gradle/index.ts
Expand Up @@ -14,6 +14,7 @@ export const defaultConfig = {
'\\.gradle(\\.kts)?$',
'(^|/)gradle\\.properties$',
'(^|/)gradle/.+\\.toml$',
'(^|/)buildSrc/.+\\.kt$',
'\\.versions\\.toml$',
// The two below is for gradle-consistent-versions plugin
`(^|/)versions.props$`,
Expand Down
105 changes: 104 additions & 1 deletion lib/modules/manager/gradle/parser.spec.ts
Expand Up @@ -2,7 +2,7 @@ import is from '@sindresorhus/is';
import { codeBlock } from 'common-tags';
import { Fixtures } from '../../../../test/fixtures';
import { fs, logger } from '../../../../test/util';
import { parseGradle, parseProps } from './parser';
import { parseGradle, parseKotlinSource, parseProps } from './parser';
import { GRADLE_PLUGINS, REGISTRY_URLS } from './parser/common';

jest.mock('../../../util/fs');
Expand Down Expand Up @@ -930,4 +930,107 @@ describe('modules/manager/gradle/parser', () => {
expect(deps).toMatchObject([output].filter(is.truthy));
});
});

describe('Kotlin object notation', () => {
it('simple objects', () => {
const input = codeBlock`
object Versions {
const val baz = "1.2.3"
}

object Libraries {
val deps = mapOf("api" to "org.slf4j:slf4j-api:\${Versions.baz}")
val dep: String = "foo:bar:" + Versions.baz
}
`;

const res = parseKotlinSource(input);
expect(res).toMatchObject({
vars: {
'Versions.baz': {
key: 'Versions.baz',
value: '1.2.3',
},
},
deps: [
{
depName: 'org.slf4j:slf4j-api',
groupName: 'Versions.baz',
currentValue: '1.2.3',
},
{
depName: 'foo:bar',
groupName: 'Versions.baz',
currentValue: '1.2.3',
},
],
});
});

it('nested objects', () => {
const input = codeBlock`
object Deps {
const val kotlinVersion = "1.5.31"

object Kotlin {
val stdlib = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:\${Deps.kotlinVersion}"
}

object Test {
private const val version = "1.3.0-rc01"
const val core = "androidx.test:core:\${Deps.Test.version}"

object Espresso {
private const val version = "3.3.0-rc01"
const val espressoCore = "androidx.test.espresso:espresso-core:$version"
}

object Androidx {
const val coreKtx = "androidx.test:core-ktx:$version"
}
}
}
`;

const res = parseKotlinSource(input);
expect(res).toMatchObject({
vars: {
'Deps.kotlinVersion': {
key: 'Deps.kotlinVersion',
value: '1.5.31',
},
'Deps.Test.version': {
key: 'Deps.Test.version',
value: '1.3.0-rc01',
},
'Deps.Test.Espresso.version': {
key: 'Deps.Test.Espresso.version',
value: '3.3.0-rc01',
},
},
deps: [
{
depName: 'org.jetbrains.kotlin:kotlin-stdlib-jdk7',
currentValue: '1.5.31',
groupName: 'Deps.kotlinVersion',
},
{
depName: 'androidx.test:core',
currentValue: '1.3.0-rc01',
groupName: 'Deps.Test.version',
},
{
depName: 'androidx.test.espresso:espresso-core',
currentValue: '3.3.0-rc01',
groupName: 'Deps.Test.Espresso.version',
},
{
depName: 'androidx.test:core-ktx',
currentValue: '1.3.0-rc01',
groupName: 'Deps.Test.version',
},
],
});
});
});
});
29 changes: 29 additions & 0 deletions lib/modules/manager/gradle/parser.ts
Expand Up @@ -5,6 +5,7 @@ import { qApplyFrom } from './parser/apply-from';
import { qAssignments } from './parser/assignments';
import { qDependencies, qLongFormDep } from './parser/dependencies';
import { setParseGradleFunc } from './parser/handlers';
import { qKotlinMultiObjectVarAssignment } from './parser/objects';
import { qPlugins } from './parser/plugins';
import { qRegistryUrls } from './parser/registry-urls';
import { qVersionCatalogs } from './parser/version-catalogs';
Expand Down Expand Up @@ -77,6 +78,34 @@ export function parseGradle(
return { deps, urls, vars };
}

export function parseKotlinSource(
input: string,
initVars: PackageVariables = {},
packageFile = ''
): { vars: PackageVariables; deps: PackageDependency<GradleManagerData>[] } {
let vars: PackageVariables = { ...initVars };
const deps: PackageDependency<GradleManagerData>[] = [];

const query = q.tree<Ctx>({
type: 'root-tree',
maxDepth: 1,
search: qKotlinMultiObjectVarAssignment,
});

const parsedResult = groovy.query(input, query, {
...ctx,
packageFile,
globalVars: vars,
});

if (parsedResult) {
deps.push(...parsedResult.deps);
vars = { ...vars, ...parsedResult.globalVars };
}

return { deps, vars };
}

const propWord = '[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*';
const propRegex = regEx(
`^(?<leftPart>\\s*(?<key>${propWord})\\s*[= :]\\s*['"]?)(?<value>[^\\s'"]+)['"]?\\s*$`
Expand Down
2 changes: 1 addition & 1 deletion lib/modules/manager/gradle/parser/assignments.ts
Expand Up @@ -138,7 +138,7 @@ const qKotlinMapOfExpr = (
);

// val versions = mapOf("foo1" to "bar1", "foo2" to "bar2", "foo3" to "bar3")
const qKotlinMultiMapOfVarAssignment = qVariableAssignmentIdentifier
export const qKotlinMultiMapOfVarAssignment = qVariableAssignmentIdentifier
.op('=')
.sym('mapOf')
.tree({
Expand Down
12 changes: 12 additions & 0 deletions lib/modules/manager/gradle/parser/common.spec.ts
Expand Up @@ -121,11 +121,23 @@ describe('modules/manager/gradle/parser/common', () => {
});

it('findVariable', () => {
ctx.tmpNestingDepth = [token, token];
ctx.globalVars = {
foo: { key: 'foo', value: 'bar' },
'test.foo': { key: 'test.foo', value: 'bar2' },
'test.test.foo3': { key: 'test.test.foo3', value: 'bar3' },
};

expect(findVariable('unknown-global-var', ctx)).toBeUndefined();
expect(findVariable('foo3', ctx)).toStrictEqual(
ctx.globalVars['test.test.foo3']
);
expect(findVariable('test.foo', ctx)).toStrictEqual(
ctx.globalVars['test.foo']
);
expect(findVariable('foo', ctx)).toStrictEqual(ctx.globalVars['test.foo']);

ctx.tmpNestingDepth = [];
expect(findVariable('foo', ctx)).toStrictEqual(ctx.globalVars['foo']);
});

Expand Down