Skip to content

Commit

Permalink
feat(gradle/manager): add support for Kotlin objects in buildSrc fi…
Browse files Browse the repository at this point in the history
…les (#21892)
  • Loading branch information
Churro committed May 10, 2023
1 parent bf2f9db commit eec46d1
Show file tree
Hide file tree
Showing 10 changed files with 358 additions and 8 deletions.
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

0 comments on commit eec46d1

Please sign in to comment.