import { isTruthy } from '@sindresorhus/is'; import { codeBlock } from 'common-tags'; import { Fixtures } from '~test/fixtures.ts'; import { fs, logger } from '~test/util.ts'; import { GRADLE_PLUGINS, GRADLE_TEST_SUITES, REGISTRY_URLS, } from './parser/common.ts'; import { parseGradle, parseJavaToolchainVersion, parseKotlinSource, parseProps, } from './parser.ts'; vi.mock('../../../util/fs/index.ts'); function mockFs(): void { fs.getSiblingFileName.mockImplementation( (existingFileNameWithPath: string, otherFileName: string) => { return existingFileNameWithPath .slice(0, existingFileNameWithPath.lastIndexOf('/') + 1) .concat(otherFileName); }, ); } describe('modules/manager/gradle/parser', () => { it('handles end of input', () => { expect(parseGradle('version = ').deps).toBeEmpty(); expect(parseGradle('id "foo.bar" version').deps).toBeEmpty(); }); describe('variables', () => { describe('Groovy: single var assignments', () => { it.each` input | name | value ${'foo = "1.2.3"'} | ${'foo'} | ${'1.2.3'} ${'foo.bar = "1.2.3"'} | ${'foo.bar'} | ${'1.2.3'} ${'foo.bar.baz = "1.2.3"'} | ${'foo.bar.baz'} | ${'1.2.3'} ${'ext.foobar = "1.2.3"'} | ${'foobar'} | ${'1.2.3'} ${'foo["bar"] = "1.2.3"'} | ${'foo.bar'} | ${'1.2.3'} ${'foo["bar"]["baz"] = "1.2.3"'} | ${'foo.bar.baz'} | ${'1.2.3'} ${'foo["bar"]["baz.qux"] = "1.2.3"'} | ${'foo.bar.baz.qux'} | ${'1.2.3'} ${'foo.bar["baz"]["qux"] = "1.2.3"'} | ${'foo.bar.baz.qux'} | ${'1.2.3'} ${'ext["foo"] = "1.2.3"'} | ${'foo'} | ${'1.2.3'} ${'ext["foo"]["bar"] = "1.2.3"'} | ${'foo.bar'} | ${'1.2.3'} ${'extra["foo"] = "1.2.3"'} | ${'foo'} | ${'1.2.3'} ${'project.foobar = "1.2.3"'} | ${'foobar'} | ${'1.2.3'} ${'project.ext.foo.bar = "1.2.3"'} | ${'foo.bar'} | ${'1.2.3'} ${'rootProject.foobar = "1.2.3"'} | ${'foobar'} | ${'1.2.3'} ${'rootProject.foo.bar = "1.2.3"'} | ${'foo.bar'} | ${'1.2.3'} `('$input', ({ input, name, value }) => { const { vars } = parseGradle(input); expect(vars).toContainKey(name); expect(vars[name]).toMatchObject({ key: name, value }); }); }); describe('Groovy: single var assignments (non-match)', () => { it.each` input ${'foo[["bar"]] = "baz"'} ${'foo["bar", "invalid"] = "1.2.3"'} ${'foo.bar["baz", "invalid"] = "1.2.3"'} `('$input', ({ input }) => { const { vars } = parseGradle(input); expect(vars).toBeEmpty(); }); }); describe('Groovy: multi var assignments', () => { it('simple map', () => { const input = codeBlock` ext { versions = [ spotbugs_annotations : '4.5.3', core : '1.7.0', ] ignored = [ 'asdf' ] libraries = [:] libraries += [ guava: "com.google.guava:guava:31.1-jre", detekt: '1.18.1', core2: versions.core ] } `; const { vars } = parseGradle(input); expect(vars).toMatchObject({ 'versions.spotbugs_annotations': { key: 'versions.spotbugs_annotations', value: '4.5.3', }, 'versions.core': { key: 'versions.core', value: '1.7.0', }, 'libraries.guava': { key: 'libraries.guava', value: 'com.google.guava:guava:31.1-jre', }, 'libraries.detekt': { key: 'libraries.detekt', value: '1.18.1', }, 'libraries.core2': { key: 'versions.core', value: '1.7.0', }, }); }); it('nested map', () => { const input = codeBlock` project.ext.versions = [ some: invalidsymbol, android: [ buildTools: '30.0.3' ], kotlin: '1.4.30', 'spring-boot': '2.7.3', androidx: [ paging: '2.1.2', kotlin: [ stdlib: '1.4.20', coroutines: '1.3.7', ], ], 'androidx': [ 'appcompat': '1.6.1', 'compose': [ 'ui': '1.6.0', 'runtime': '1.6.0', ], ], espresso: '3.2.0' ] `; const { vars } = parseGradle(input); expect(vars).toMatchObject({ 'versions.android.buildTools': { key: 'versions.android.buildTools', value: '30.0.3', }, 'versions.kotlin': { key: 'versions.kotlin', value: '1.4.30', }, 'versions.spring-boot': { key: 'versions.spring-boot', value: '2.7.3', }, 'versions.androidx.paging': { key: 'versions.androidx.paging', value: '2.1.2', }, 'versions.androidx.kotlin.stdlib': { key: 'versions.androidx.kotlin.stdlib', value: '1.4.20', }, 'versions.androidx.kotlin.coroutines': { key: 'versions.androidx.kotlin.coroutines', value: '1.3.7', }, 'versions.androidx.appcompat': { key: 'versions.androidx.appcompat', value: '1.6.1', }, 'versions.androidx.compose.ui': { key: 'versions.androidx.compose.ui', value: '1.6.0', }, 'versions.androidx.compose.runtime': { key: 'versions.androidx.compose.runtime', value: '1.6.0', }, 'versions.espresso': { key: 'versions.espresso', value: '3.2.0', }, }); }); it('map with interpolated dependency strings', () => { const input = codeBlock` def slfj4Version = "2.0.0" def lifecycle_version = "2.5.1" libraries = [ jcl: "org.slf4j:jcl-over-slf4j:\${slfj4Version}", releaseCoroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.26.1-eap13" api: "org.slf4j:slf4j-api:$slfj4Version", lifecycle: [ "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version", "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" ] ] foo = [ group: "org.slf4j", name: "slf4j-ext", version: slfj4Version ] `; const { deps } = parseGradle(input); expect(deps).toMatchObject([ { depName: 'org.slf4j:jcl-over-slf4j', sharedVariableName: 'slfj4Version', currentValue: '2.0.0', }, { depName: 'org.jetbrains.kotlinx:kotlinx-coroutines-core', sharedVariableName: 'libraries.releaseCoroutines', currentValue: '0.26.1-eap13', }, { depName: 'org.slf4j:slf4j-api', sharedVariableName: 'slfj4Version', currentValue: '2.0.0', }, { depName: 'androidx.lifecycle:lifecycle-runtime-ktx', sharedVariableName: 'lifecycle_version', currentValue: '2.5.1', }, { depName: 'androidx.lifecycle:lifecycle-viewmodel-ktx', sharedVariableName: 'lifecycle_version', currentValue: '2.5.1', }, { depName: 'org.slf4j:slf4j-ext', sharedVariableName: 'slfj4Version', currentValue: '2.0.0', }, ]); }); }); describe('Kotlin: single var assignments', () => { it.each` input | name | value ${'set("foo", "1.2.3")'} | ${'foo'} | ${'1.2.3'} ${'version("foo", "1.2.3")'} | ${'foo'} | ${'1.2.3'} `('$input', ({ input, name, value }) => { const { vars } = parseGradle(input); expect(vars).toContainKey(name); expect(vars[name]).toMatchObject({ key: name, value }); }); }); describe('Kotlin: single var assignments (non-match)', () => { it.each` input ${'set(["foo", "bar"])'} ${'set("foo", "bar", "baz", "qux"])'} `('$input', ({ input }) => { const { vars } = parseGradle(input); expect(vars).toBeEmpty(); }); }); describe('Kotlin: single extra var assignments', () => { it.each` input | name | value ${'val foo by extra("1.2.3")'} | ${'foo'} | ${'1.2.3'} ${'val foo by extra { "1.2.3" }'} | ${'foo'} | ${'1.2.3'} ${'val foo: String by extra { "1.2.3" }'} | ${'foo'} | ${'1.2.3'} `('$input', ({ input, name, value }) => { const { vars } = parseGradle(input); expect(vars).toContainKey(name); expect(vars[name]).toMatchObject({ key: name, value }); }); }); describe('Kotlin: multi var assignments', () => { it('simple map', () => { const input = 'val versions = mapOf("foo1" to "bar1", "foo2" to "bar2", "foo3" to "bar3")'; const { vars } = parseGradle(input); expect(vars).toMatchObject({ 'versions.foo1': { key: 'versions.foo1', value: 'bar1', }, 'versions.foo2': { key: 'versions.foo2', value: 'bar2', }, 'versions.foo3': { key: 'versions.foo3', value: 'bar3', }, }); }); it('nested map', () => { const input = codeBlock` val junitPlatformVersion: String by extra { "1.0.1" } ext["deps"] = mapOf( "support" to mapOf( "appCompat" to "com.android.support:appcompat-v7:26.0.2", "invalid" to whatever, "junit" to mapOf( "jupiter" to "5.0.1", "platform" to "1.0.1", ) "design" to "com.android.support:design:26.0.2" ), "support2" to mapOfInvalid( "design2" to "com.android.support:design:26.0.2" ), "picasso" to "com.squareup.picasso:picasso:2.5.2", "junit-platform-runner" to junitPlatformVersion ) `; const { vars } = parseGradle(input); expect(vars).toMatchObject({ 'deps.support.appCompat': { key: 'deps.support.appCompat', value: 'com.android.support:appcompat-v7:26.0.2', }, 'deps.support.junit.jupiter': { key: 'deps.support.junit.jupiter', value: '5.0.1', }, 'deps.support.junit.platform': { key: 'deps.support.junit.platform', value: '1.0.1', }, 'deps.support.design': { key: 'deps.support.design', value: 'com.android.support:design:26.0.2', }, 'deps.picasso': { key: 'deps.picasso', value: 'com.squareup.picasso:picasso:2.5.2', }, 'deps.junit-platform-runner': { key: 'junitPlatformVersion', value: '1.0.1', }, }); }); it('map with interpolated dependency strings', () => { const input = codeBlock` val slfj4Version = "2.0.0" libraries = mapOf( "jcl" to "org.slf4j:jcl-over-slf4j:\${slfj4Version}", "releaseCoroutines" to "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.26.1-eap13" "api" to "org.slf4j:slf4j-api:$slfj4Version", ) `; const { deps } = parseGradle(input); expect(deps).toMatchObject([ { depName: 'org.slf4j:jcl-over-slf4j', sharedVariableName: 'slfj4Version', currentValue: '2.0.0', }, { depName: 'org.jetbrains.kotlinx:kotlinx-coroutines-core', sharedVariableName: 'libraries.releaseCoroutines', currentValue: '0.26.1-eap13', }, { depName: 'org.slf4j:slf4j-api', sharedVariableName: 'slfj4Version', currentValue: '2.0.0', }, ]); }); }); }); describe('dependencies', () => { describe('simple dependency strings', () => { it.each` input | output ${'"foo:bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'"foo:bar:1.2+"'} | ${{ depName: 'foo:bar', currentValue: '1.2+' }} ${'"foo:bar:1.2.3@zip"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'zip' }} ${'"foo:bar:1.2.3:docs@jar"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'jar' }} ${'"foo:bar1:1"'} | ${{ depName: 'foo:bar1', currentValue: '1', managerData: { fileReplacePosition: 10 } }} ${'"foo:bar:[1.2.3, )"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, )', managerData: { fileReplacePosition: 9 } }} ${'"foo:bar:[1.2.3, 1.2.4)"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, 1.2.4)', managerData: { fileReplacePosition: 9 } }} ${'"foo:bar:[,1.2.4)"'} | ${{ depName: 'foo:bar', currentValue: '[,1.2.4)', managerData: { fileReplacePosition: 9 } }} ${'"foo:bar:[1.2.3, )!!1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, )!!1.2.3', managerData: { fileReplacePosition: 9 } }} ${'"foo:bar:[1.2.3, 1.2.4)!!1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '[1.2.3, 1.2.4)!!1.2.3', managerData: { fileReplacePosition: 9 } }} ${'"foo:bar:[,1.2.4)!!1.2.4"'} | ${{ depName: 'foo:bar', currentValue: '[,1.2.4)!!1.2.4', managerData: { fileReplacePosition: 9 } }} ${'"foo:bar:x86@x86"'} | ${{ depName: 'foo:bar', currentValue: 'x86', managerData: { fileReplacePosition: 9 } }} ${'foo.bar = "foo:bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} `('$input', ({ input, output }) => { const { deps } = parseGradle(input); expect(deps).toMatchObject([output]); }); }); describe('interpolated dependency strings', () => { it.each` def | str | output ${'foo = "1.2.3"'} | ${'"foo:bar:$foo@@@"'} | ${null} ${''} | ${'"foo:bar:$baz"'} | ${null} ${'foo = "1"; bar = "2"; baz = "3"'} | ${'"foo:bar:$foo.$bar.$baz"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', skipReason: 'contains-variable' }} ${'baz = "1.2.3"'} | ${'"foo:bar:$baz"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'foo.bar = "1.2.3"'} | ${'"foo:bar:$foo.bar"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'foo.bar' }} ${'foo = "1.2.3"'} | ${'"foo:bar_$foo:4.5.6"'} | ${{ depName: 'foo:bar_1.2.3', managerData: { fileReplacePosition: 28 } }} ${'foo = "bar"'} | ${'"foo:${foo}1:1"'} | ${{ depName: 'foo:bar1', currentValue: '1', managerData: { fileReplacePosition: 25 } }} ${'bar = "bar:1.2.3"'} | ${'"foo:$bar"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', skipReason: 'contains-variable' }} ${'baz = "1.2.3"'} | ${'foobar = "foo:bar:$baz"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'foo = "${bar}"; baz = "1.2.3"'} | ${'"foo:bar:${baz}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'"foo:bar:${ext[\'baz\']}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'"foo:bar:${ext.baz}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'"foo:bar:${project.ext[\'baz\']}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'a = "foo"; b = "bar"; c="1.2.3"'} | ${'"${a}:${b}:${property("c")}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'c' }} ${'a = "foo"; b = "bar"; c="1.2.3"'} | ${'"${a}:${b}:${properties["c"]}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'c' }} `('$def | $str', ({ def, str, output }) => { const { deps } = parseGradle([def, str].join('\n')); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); describe('concatenated dependency strings', () => { it.each` def | str | output ${''} | ${'"foo:bar" + ":1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 15 } }} ${''} | ${'"foo:bar:" + "1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 15 } }} ${''} | ${'"foo:bar:" + "1.2.3@zip"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'zip', managerData: { fileReplacePosition: 15 } }} ${''} | ${'"foo:" + "bar:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 15 } }} ${''} | ${'"foo:bar:1." + "2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', skipReason: 'contains-variable' }} ${''} | ${'"foo:bar1.2.3:" + "1.2.3"'} | ${{ depName: 'foo:bar1.2.3', currentValue: '1.2.3', managerData: { fileReplacePosition: 20 } }} ${'baz = "1.2.3"'} | ${'"foo:bar:" + baz'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 7 } }} ${'baz = "1.2.3"'} | ${'"foo:bar:" + property("baz")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 7 } }} ${'baz = "1.2.3"'} | ${'"foo:bar:" + baz + "456"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3456', skipReason: 'contains-variable' }} ${'baz = "1.2.3"'} | ${'"foo:bar:" + baz + "@zip"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', dataType: 'zip', managerData: { fileReplacePosition: 7 } }} ${'foo.bar = "bar:"; baz = "1.2.3"'} | ${'"foo:" + foo.bar + "${baz}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 25 } }} ${'foo.bar = "bar"; baz = "1.2.3"'} | ${'"foo:bar_${foo.bar}:" + baz'} | ${{ depName: 'foo:bar_bar', currentValue: '1.2.3', managerData: { fileReplacePosition: 24 } }} `('$def | $str', ({ def, str, output }) => { const { deps } = parseGradle([def, str].join('\n')); expect(deps).toMatchObject([output]); }); }); describe('property accessors', () => { it.each` accessor ${'property'} ${'getProperty'} ${'ext.getProperty'} ${'extra.get'} ${'project.property'} ${'project.getProperty'} ${'project.ext.getProperty'} ${'project.ext.get'} ${'project.extra.get'} ${'rootProject.property'} ${'rootProject.getProperty'} ${'rootProject.ext.getProperty'} ${'rootProject.extra.get'} `('$accessor', ({ accessor }) => { const input = ` baz = "1.2.3" api("foo:bar:$\{${String(accessor)}("baz") as String}") `; const { deps } = parseGradle(input); expect(deps).toMatchObject([ { depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz', }, ]); }); }); describe('kotlin() short notation dependencies', () => { const output = { depName: 'foo', packageName: 'org.jetbrains.kotlin:kotlin-foo', currentValue: '1.2.3', }; it.each` def | str | output ${''} | ${'kotlin("foo", "1.2.3")'} | ${output} ${''} | ${'kotlin("foo", version = "1.2.3")'} | ${output} ${'some = "foo"'} | ${'kotlin(some, version = "1.2.3")'} | ${output} ${'some = "foo"'} | ${'kotlin("${some}", "1.2.3")'} | ${output} ${'baz = "1.2.3"'} | ${'kotlin("foo", baz)'} | ${{ ...output, sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'kotlin("foo", version = baz)'} | ${output} ${'baz = "1.2.3"'} | ${'kotlin("foo", property("baz"))'} | ${output} ${'baz = "1.2.3"'} | ${'kotlin("foo", "${baz}456")'} | ${{ skipReason: 'unspecified-version' }} ${'baz = "1.2.3"'} | ${'kotlin("foo", baz + "456")'} | ${{ skipReason: 'unspecified-version' }} ${''} | ${'kotlin("foo", some)'} | ${null} ${''} | ${'kotlin(["foo", "1.2.3"])'} | ${null} ${''} | ${'kotlin("foo", "1.2.3", "4.5.6")'} | ${null} ${''} | ${'kotlin("foo", "1.2.3@@@")'} | ${null} `('$def | $str', ({ def, str, output }) => { const { deps } = parseGradle([def, str].join('\n')); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); describe('map notation dependencies', () => { it.each` def | str | output ${''} | ${'group: "foo", name: "bar", version: "1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${''} | ${'group: "foo", name: "bar", version: baz'} | ${null} ${''} | ${'group: "foo", name: "bar", version: "1.2.3@@@"'} | ${null} ${'baz = "1.2.3"'} | ${'group: "foo", name: "bar", version: baz'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'some = "foo"'} | ${'group: property("some"), name: property("some"), version: "1.2.3"'} | ${{ depName: 'foo:foo', currentValue: '1.2.3' }} ${'some = "foo"'} | ${'group: some, name: some, version: "1.2.3"'} | ${{ depName: 'foo:foo', currentValue: '1.2.3' }} ${'some = "foo"'} | ${'group: "${some}", name: "${some}", version: "1.2.3"'} | ${{ depName: 'foo:foo', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'group: "foo", name: "bar", version: "${baz}"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'group: "foo", name: "bar", version: "${baz}456"'} | ${{ depName: 'foo:bar', skipReason: 'unspecified-version' }} ${''} | ${'(group: "foo", name: "bar", version: "1.2.3", classifier: "sources")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${''} | ${'(group: "foo", name: "bar", version: "1.2.3") {exclude module: "spring-jcl"}'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${''} | ${"implementation platform(group: 'foo', name: 'bar', version: '1.2.3')"} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${''} | ${'(group = "foo", name = "bar", version = "1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'(group = "foo", name = "bar", version = baz)'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'some = "foo"'} | ${'(group = some, name = some, version = "1.2.3")'} | ${{ depName: 'foo:foo', currentValue: '1.2.3' }} ${'some = "foo"'} | ${'(group = "${some}", name = "${some}", version = "1.2.3")'} | ${{ depName: 'foo:foo', currentValue: '1.2.3' }} ${'some = "foo"'} | ${'(group = "${some}" + some, name = some + "bar" + some, version = "1.2.3")'} | ${{ depName: 'foofoo:foobarfoo', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'(group = "foo", name = "bar", version = "${baz}")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'(group = "foo", name = "bar", version = "${baz}456")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3456', skipReason: 'unspecified-version' }} ${'baz = "1.2.3"'} | ${'(group = "foo", name = "bar", version = baz + "456")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3456', skipReason: 'unspecified-version' }} ${''} | ${'(group = "foo", name = "bar", version = "1.2.3", changing: true)'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} `('$def | $str', ({ def, str, output }) => { const { deps } = parseGradle([def, str].join('\n')); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); describe('dependencySet dependencies', () => { it('simple dependencySet', () => { const input = codeBlock` ext.activemq_version = "5.8.0" dependencySet(group: 'org.apache.activemq', version: activemq_version) { entry 'activemq-broker' entry('activemq-kahadb-store') { exclude group: "org.springframework", name: "spring-context" } entry 'activemq-stomp' } `; const { deps } = parseGradle(input); expect(deps).toMatchObject([ { depName: 'org.apache.activemq:activemq-broker', currentValue: '5.8.0', sharedVariableName: 'activemq_version', }, { depName: 'org.apache.activemq:activemq-kahadb-store', currentValue: '5.8.0', sharedVariableName: 'activemq_version', }, { depName: 'org.apache.activemq:activemq-stomp', currentValue: '5.8.0', sharedVariableName: 'activemq_version', }, ]); }); describe('dependencySet variants', () => { const validOutput = [ { depName: 'foo:bar1', currentValue: '1.2.3', sharedVariableName: 'foo:1.2.3', }, { depName: 'foo:bar2', currentValue: '1.2.3', sharedVariableName: 'foo:1.2.3', }, ]; const validOutput1 = validOutput.map((dep) => { return { ...dep, sharedVariableName: 'baz' }; }); it.each` def | str | output ${''} | ${'dependencySet([group: "foo", version: "1.2.3"]) { entry "bar1" }'} | ${{}} ${''} | ${'dependencySet(group: "foo", version: "1.2.3", group: "foo", version: "1.2.3") { entry "bar1" }'} | ${{}} ${''} | ${'dependencySet(group: "foo", version: "1.2.3") { { entry "bar1" } }'} | ${{}} ${''} | ${'dependencySet(group: "foo", version: "1.2.3") { entry(["bar1"]) }'} | ${{}} ${''} | ${'dependencySet(group: "foo", version: "1.2.3") { entry("bar", "baz") }'} | ${{}} ${''} | ${'dependencySet(group: "${nonexistingvar}", version: "1.2.3") { entry "bar1"; entry "bar2" }'} | ${{}} ${''} | ${'dependencySet(group: "foo", version: "1.2.3") { entry "bar1"; entry "bar2" }'} | ${validOutput} ${''} | ${'dependencySet(group: "foo", version: "1.2.3") { entry "bar1"; entry ("bar2") }'} | ${validOutput} ${'baz = "1.2.3"'} | ${'dependencySet(group: "foo", version: baz) { entry "bar1"; entry ("bar2") }'} | ${validOutput1} ${'baz = "1.2.3"'} | ${'dependencySet(group: "foo", version: "${baz}") { entry "bar1"; entry ("bar2") }'} | ${validOutput1} ${'baz = "1.2.3"'} | ${'dependencySet(group: "foo", version: property("baz")) { entry "bar1"; entry ("bar2") }'} | ${validOutput1} ${'some = "foo"; other = "bar1"'} | ${'dependencySet(group: some, version: "1.2.3") { entry other; entry "bar2" }'} | ${validOutput} ${'some = "foo"; baz = "1.2.3"'} | ${'dependencySet(group: some, version: "${baz}456") { entry "bar1"; entry "bar2" }'} | ${{}} ${'some = "foo"; other = "bar1"'} | ${'dependencySet(group: some, version: "1.2.3") { entry(other); entry "bar2" }'} | ${validOutput} ${'some = "foo"; other = "bar1"'} | ${'dependencySet(group: "${some}", version: "1.2.3") { entry "${other}"; entry "bar2" }'} | ${validOutput} ${'some = "foo"; other = "bar1"'} | ${'dependencySet(group: "${some}", version: "1.2.3") { entry("${other}"); entry "bar2" }'} | ${validOutput} ${''} | ${'dependencySet(group = "foo", version = "1.2.3") { entry "bar1"; entry "bar2" }'} | ${validOutput} `('$def | $str', ({ def, str, output }) => { const { deps } = parseGradle([def, str].join('\n')); expect(deps).toMatchObject(output); }); }); }); describe('dependencySubstitution constructs', () => { it.each` input | output ${'substitute module("foo:bar:1.2.3")'} | ${null} ${'substitute(module("foo:bar:1.2.3"))'} | ${null} ${'substitute module("foo:bar:1.2.3") with module("bar:qux:4.5.6")'} | ${{ depName: 'bar:qux', currentValue: '4.5.6' }} ${'substitute(module("foo:bar:1.2.3")).using(module("bar:qux:4.5.6"))'} | ${{ depName: 'bar:qux', currentValue: '4.5.6' }} `('$input', ({ input, output }) => { const { deps } = parseGradle(input); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); describe('plugins', () => { it.each` def | input | output ${''} | ${'id "foo.bar" version "1.2.3"'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'id("foo.bar").version("1.2.3")'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'id(["foo.bar"]) version "1.2.3"'} | ${null} ${''} | ${'id("foo", "bar") version "1.2.3"'} | ${null} ${''} | ${'id(foo) version "1.2.3"'} | ${null} ${''} | ${'id "foo".version("1.2.3")'} | ${null} ${''} | ${'id("foo.bar") version("1.2.3")'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'id("foo.bar") version "1.2.3"'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'id "foo.bar" version "$baz"'} | ${{ depName: 'foo.bar', skipReason: 'unspecified-version', currentValue: 'baz' }} ${'baz = "1.2.3"'} | ${'id "foo.bar" version "$baz"'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'id("foo.bar") version "$baz"'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${'foo = "bar"'} | ${'id(foo + ".baz") version "1.2.3"'} | ${{ depName: 'bar.baz', packageName: 'bar.baz:bar.baz.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'id "foo.bar" version "x${ab}cd"'} | ${{ depName: 'foo.bar', skipReason: 'unspecified-version' }} ${''} | ${'id("foo.bar") version "$baz"'} | ${{ depName: 'foo.bar', skipReason: 'unspecified-version', currentValue: 'baz' }} ${''} | ${'id("foo.bar") version "x${ab}cd"'} | ${{ depName: 'foo.bar', skipReason: 'unspecified-version' }} ${''} | ${'id("foo.bar") version "1" + "2.3"'} | ${{ depName: 'foo.bar', skipReason: 'unspecified-version' }} ${''} | ${'id("foo.bar") version property("qux")'} | ${{ depName: 'foo.bar', skipReason: 'unspecified-version' }} ${'baz = "1.2.3"'} | ${'id("foo.bar") version property("baz")'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'id "foo.bar" version baz'} | ${{ depName: 'foo.bar', currentValue: 'baz', skipReason: 'unspecified-version' }} ${'baz = "1.2.3"'} | ${'id "foo.bar" version baz'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'id("foo.bar") version baz'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'id("foo.bar").version(baz)'} | ${{ depName: 'foo.bar', packageName: 'foo.bar:foo.bar.gradle.plugin', currentValue: '1.2.3' }} ${''} | ${'kotlin("jvm") version "1.3.71"'} | ${{ depName: 'org.jetbrains.kotlin.jvm', packageName: 'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin', currentValue: '1.3.71' }} ${'baz = "1.3.71"'} | ${'kotlin("jvm") version baz'} | ${{ depName: 'org.jetbrains.kotlin.jvm', packageName: 'org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin', currentValue: '1.3.71', sharedVariableName: 'baz' }} `('$def | $input', ({ def, input, output }) => { const { deps } = parseGradle([def, input].join('\n')); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); }); describe('registries', () => { describe('predefined registries', () => { it.each` input | output ${'mavenCentral()'} | ${REGISTRY_URLS.mavenCentral} ${'google()'} | ${REGISTRY_URLS.google} ${'google { content { includeGroup "foo" } }'} | ${REGISTRY_URLS.google} ${'gradlePluginPortal()'} | ${REGISTRY_URLS.gradlePluginPortal} ${'jcenter()'} | ${REGISTRY_URLS.jcenter} `('$input', ({ input, output }) => { const { urls } = parseGradle(input); expect(urls).toMatchObject([{ registryUrl: output }]); }); }); describe('custom registries', () => { it.each` def | input | url ${''} | ${'maven("")'} | ${null} ${''} | ${'maven(["https://foo.bar/baz/qux"])'} | ${null} ${''} | ${'maven("foo", "bar")'} | ${null} ${''} | ${'maven("https://foo.bar/baz")'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven("${base}/baz")'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven(base)'} | ${'https://foo.bar'} ${''} | ${'maven(url = "https://foo.bar/baz")'} | ${'https://foo.bar/baz'} ${''} | ${'maven(url = uri("https://foo.bar/baz"))'} | ${'https://foo.bar/baz'} ${''} | ${'maven(uri("https://foo.bar/baz"))'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven(uri("${base}/baz"))'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven(uri(property("base")))'} | ${'https://foo.bar'} ${'base="https://foo.bar"'} | ${'maven(uri(base))'} | ${'https://foo.bar'} ${'base="https://foo.bar"'} | ${'maven(uri(base + "/baz"))'} | ${'https://foo.bar/baz'} ${''} | ${'maven(uri(["https://foo.bar/baz"]))'} | ${null} ${''} | ${'maven { ["https://foo.bar/baz"] }'} | ${null} ${''} | ${'maven { name = "baz" }'} | ${null} ${''} | ${'maven { url "https://foo.bar/baz" }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url base + "/baz" }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url "${base}/baz" }'} | ${'https://foo.bar/baz'} ${''} | ${'maven { url uri("https://foo.bar/baz") }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url uri("${base}/baz") }'} | ${'https://foo.bar/baz'} ${''} | ${'maven { url = "https://foo.bar/baz" }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url = "${base}/baz" }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url = property("base") }'} | ${'https://foo.bar'} ${'base="https://foo.bar"'} | ${'maven { url = base }'} | ${'https://foo.bar'} ${''} | ${'maven { url = uri("https://foo.bar/baz") }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url = uri("${base}/baz") }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { url = uri(base) }'} | ${'https://foo.bar'} ${''} | ${'maven { uri(["https://foo.bar/baz"]) }'} | ${null} ${'base="https://foo.bar"'} | ${'maven { name "baz"\nurl = "${base}/${name}" }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { name "${base}" + "/baz"\nurl = "${name}" + "/qux" }'} | ${'https://foo.bar/baz/qux'} ${'base="https://foo.bar"'} | ${'maven { name = "baz"\nurl = "${base}/${name}" }'} | ${'https://foo.bar/baz'} ${'some="baz"'} | ${'maven { name = "${some}"\nurl = "https://foo.bar/${name}" }'} | ${'https://foo.bar/baz'} ${'some="foo.bar/baz"'} | ${'maven { name = property("some")\nurl = "https://${name}" }'} | ${'https://foo.bar/baz'} ${'some="baz"'} | ${'maven { name = some\nurl = "https://foo.bar/${name}" }'} | ${'https://foo.bar/baz'} ${''} | ${'maven { setUrl("https://foo.bar/baz") }'} | ${'https://foo.bar/baz'} ${''} | ${'maven { setUrl(uri("https://foo.bar/baz")) }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { setUrl("${base}/baz") }'} | ${'https://foo.bar/baz'} ${'base="https://foo.bar"'} | ${'maven { setUrl(project.property("base")) }'} | ${'https://foo.bar'} ${'base="https://foo.bar"'} | ${'maven { setUrl(base) }'} | ${'https://foo.bar'} ${''} | ${'maven { setUrl(["https://foo.bar/baz"]) }'} | ${null} ${''} | ${'maven { setUrl("foo", "bar") }'} | ${null} ${'base="https://foo.bar"'} | ${'publishing { repositories { maven("${base}/baz") } }'} | ${null} `('$def | $input', ({ def, input, url }) => { const expected = url ? [{ registryUrl: url }] : []; const { urls } = parseGradle([def, input].join('\n')); expect(urls).toMatchObject(expected); }); }); it('pluginManagement', () => { const input = codeBlock` pluginManagement { def fooVersion = "1.2.3" repositories { mavenLocal() maven { url = "https://foo.bar/plugins" } gradlePluginPortal() } plugins { id("foo.bar") version "$fooVersion" } } dependencyResolutionManagement { repositories { maven { url = "https://foo.bar/deps" } mavenCentral() } } `; const { deps, urls } = parseGradle(input); expect(deps).toMatchObject([ { depType: 'plugin', depName: 'foo.bar', currentValue: '1.2.3', }, ]); expect(urls).toMatchObject([ { registryUrl: 'https://foo.bar/plugins', scope: 'plugin', }, { registryUrl: REGISTRY_URLS.gradlePluginPortal, scope: 'plugin', }, { registryUrl: 'https://foo.bar/deps', scope: 'dep', }, { registryUrl: REGISTRY_URLS.mavenCentral, scope: 'dep', }, ]); }); describe('content descriptors', () => { it('valid combinations', () => { const input = codeBlock` maven(url = "https://foo.bar/baz") { content { excludeGroup("baz.qux") } } mavenCentral().content { includeGroup("foo.bar") } maven { url = "https://foo.bar/deps" content { includeGroupAndSubgroups("foo.bar") } } maven { url = "https://some.foo" content { includeModule("foo", "bar") excludeModule("baz", "qux") includeVersion("foo", "bar", "1.2.3") excludeVersion("baz", "qux", "4.5.6") includeGroupByRegex("org\\\\.jetbrains\\\\.kotlin.*") excludeGroupByRegex(".*google.*") includeModuleByRegex(".*foo.*", ".*bar.*") excludeModuleByRegex(".*baz.*", ".*qux.*") includeVersionByRegex(".*foo.*", ".*bar.*", "1.2.3") excludeVersionByRegex(".*baz.*", ".*qux.*", ".*4.5.*") } } `; const { urls } = parseGradle(input); expect(urls).toStrictEqual([ { registryUrl: 'https://foo.bar/baz', registryType: 'regular', scope: 'dep', content: [ { mode: 'exclude', matcher: 'simple', groupId: 'baz.qux' }, ], }, { registryUrl: REGISTRY_URLS.mavenCentral, registryType: 'regular', scope: 'dep', content: [ { mode: 'include', matcher: 'simple', groupId: 'foo.bar' }, ], }, { registryUrl: 'https://foo.bar/deps', registryType: 'regular', scope: 'dep', content: [ { mode: 'include', matcher: 'subgroup', groupId: 'foo.bar' }, ], }, { registryUrl: 'https://some.foo', registryType: 'regular', scope: 'dep', content: [ { mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', }, { mode: 'exclude', matcher: 'simple', groupId: 'baz', artifactId: 'qux', }, { mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3', }, { mode: 'exclude', matcher: 'simple', groupId: 'baz', artifactId: 'qux', version: '4.5.6', }, { mode: 'include', matcher: 'regex', groupId: '^org\\.jetbrains\\.kotlin.*$', }, { mode: 'exclude', matcher: 'regex', groupId: '^.*google.*$' }, { mode: 'include', matcher: 'regex', groupId: '^.*foo.*$', artifactId: '^.*bar.*$', }, { mode: 'exclude', matcher: 'regex', groupId: '^.*baz.*$', artifactId: '^.*qux.*$', }, { mode: 'include', matcher: 'regex', groupId: '^.*foo.*$', artifactId: '^.*bar.*$', version: '^1.2.3$', }, { mode: 'exclude', matcher: 'regex', groupId: '^.*baz.*$', artifactId: '^.*qux.*$', version: '^.*4.5.*$', }, ], }, ]); }); describe('invalid or unsupported regEx patterns', () => { it.each` fieldName | pattern ${'group'} | ${'includeGroupByRegex(".*so\\me.invalid.pattern.*")'} ${'group'} | ${'includeModuleByRegex(".*so\\me.invalid.pattern.*", ".*bar.*")'} ${'module'} | ${'includeModuleByRegex(".*foo.*", ".*so\\me.invalid.pattern.*")'} ${'module'} | ${'excludeModuleByRegex(".*baz.*", "(?!(foo|bar).*)")'} ${'version'} | ${'includeVersionByRegex(".*foo.*", ".*bar.*", "(?!(foo|bar).*)")'} ${'version'} | ${'excludeVersionByRegex(".*baz.*", ".*qux.*", "(?!(foo|bar).*)")'} `('$pattern', ({ fieldName, pattern }) => { const input = codeBlock` mavenCentral { content { ${pattern} } } `; parseGradle(input); expect(logger.logger.debug).toHaveBeenCalledWith( expect.stringContaining( `Skipping content descriptor with unsupported regExp pattern for ${fieldName}`, ), ); }); }); }); it('exclusiveContent', () => { const input = codeBlock` pluginManagement { repositories { exclusiveContent { forRepository { maven { name = "some maven registry" setUrl("https://foo.bar.com/repository/public/") } } filter { includeGroup("com.foo.bar") includeModule("foo", "bar") } } gradlePluginPortal() } } exclusiveContent { filter { includeGroup("foo") } forRepository { mavenCentral() } } exclusiveContent { forRepository { maven("https://private.repo/packages") } filter { includeVersionByRegex("com.myorg.*", ".+", "^(.(?!-SNAPSHOT))+$") } } `; const { urls } = parseGradle(input); expect(logger.logger.debug).toHaveBeenCalledWith( 'Skipping exclusive registry https://private.repo/packages with unsupported content descriptors', ); expect(urls).toMatchObject([ { registryUrl: 'https://foo.bar.com/repository/public/', registryType: 'exclusive', scope: 'plugin', content: [ { mode: 'include', matcher: 'simple', groupId: 'com.foo.bar', }, { mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', }, ], }, { registryUrl: REGISTRY_URLS.gradlePluginPortal, registryType: 'regular', scope: 'plugin', }, { registryUrl: REGISTRY_URLS.mavenCentral, registryType: 'exclusive', scope: 'dep', content: [ { mode: 'include', matcher: 'simple', groupId: 'foo', }, ], }, ]); }); }); describe('version catalog', () => { it.each` def | str | output ${''} | ${'library("foo.bar", "foo", "bar").version("1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'library("foo.bar", "foo", "bar").version(baz)'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'library("foo.bar", "foo", "bar").version("${baz}")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'library("foo.bar", "foo", "bar").version(property("baz"))'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'library("foo.bar", "foo", "bar").version("${baz}xy")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3xy', skipReason: 'unspecified-version' }} ${'baz = "1.2.3"'} | ${'library("foo.bar", "foo", "bar").version(baz + ".45")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3.45', skipReason: 'unspecified-version' }} ${'group = "foo"; artifact = "bar"'} | ${'library("foo.bar", group, artifact).version("1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'f = "foo"; b = "bar"'} | ${'library("foo.bar", "${f}", "${b}").version("1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'f = "foo"; b = "bar"; v = "1.2.3"'} | ${'library("foo.bar", property("f"), "${b}").version(v)'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'f = "foo"; b = "bar"'} | ${'library("foo.bar", "${f}" + f, "${b}").version("1.2.3")'} | ${{ depName: 'foofoo:bar', currentValue: '1.2.3' }} ${'version("baz", "1.2.3")'} | ${'library("foo.bar", "foo", "bar").versionRef("baz")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3', sharedVariableName: 'baz' }} ${''} | ${'library("foo.bar", "foo:bar:1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'group = "foo"; artifact = "bar"'} | ${'library("foo.bar", group + ":" + artifact + ":1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'library("foo-bar_baz-qux", "foo", "bar")'} | ${'"${libs.foo.bar.baz.qux}:1.2.3"'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${''} | ${'library(["foo.bar", "foo", "bar"]).version("1.2.3")'} | ${null} ${''} | ${'library("foo", "bar", "baz", "qux").version("1.2.3")'} | ${null} ${''} | ${'library("foo.bar", "foo", "bar").version("1.2.3", "4.5.6")'} | ${null} ${''} | ${'library("foo", bar, "baz").version("1.2.3")'} | ${null} ${''} | ${'library("foo", "bar" + baz).version("1.2.3")'} | ${null} ${''} | ${'plugin("foo.bar", "foo")'} | ${null} ${''} | ${'plugin("foo.bar", "foo").version("1.2.3")'} | ${{ depName: 'foo', currentValue: '1.2.3' }} ${''} | ${'alias("foo.bar").to("foo", "bar").version("1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'version("baz", "1.2.3")'} | ${'alias("foo.bar").to("foo", "bar").versionRef("baz")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'alias("foo.bar").to("foo", "bar").version("${baz}")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'f = "foo"; b = "bar"; v = "1.2.3"'} | ${'alias("foo.bar").to(f, b).version(v)'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'f = "foo"; b = "bar"; v = "1.2.3"'} | ${'alias("foo.bar").to(f + b, b + f).version(v)'} | ${{ depName: 'foobar:barfoo', currentValue: '1.2.3' }} ${'f = "foo"; b = "bar"; v = "1.2.3"'} | ${'alias("foo.bar").to("${f}", "${b}").version("$v")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${''} | ${'alias(["foo.bar"]).to("foo", "bar").version("1.2.3")'} | ${null} `('$def | $str', ({ def, str, output }) => { const input = [def, str].join('\n'); const { deps } = parseGradle(`versionCatalogs { libs { ${input} } }`); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); describe('heuristic dependency matching', () => { it.each` input | output ${'("foo", "bar", "1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'("foo", "bar", "1.2.3", "4.5.6")'} | ${null} ${'(["foo", "bar", "1.2.3"])'} | ${null} ${'someMethod("foo", "bar", "1.2.3")'} | ${{ depName: 'foo:bar', currentValue: '1.2.3' }} ${'listOf("foo", "bar", "baz")'} | ${null} ${'resValue("string", "google_maps_key", "FakeGoogleMapsApiKey123")'} | ${null} ${'buildConfigField("int", "MAX_RETRIES", "3")'} | ${null} ${'java { registerFeature(foo) { capability("foo", "bar", "1.2.3") } }'} | ${null} `('$input', ({ input, output }) => { const { deps } = parseGradle(input); expect(deps).toMatchObject([output].filter(isTruthy)); }); it('handles 3 independent dependencies mismatched as groupId, artifactId, version', () => { const { deps } = parseGradle( 'someConfig("foo:bar:1.2.3", "foo:baz:4.5.6", "foo:qux:7.8.9")', ); expect(deps).toMatchObject([ { depName: 'foo:bar', currentValue: '1.2.3' }, { depName: 'foo:baz', currentValue: '4.5.6' }, { depName: 'foo:qux', currentValue: '7.8.9' }, ]); }); }); describe('calculations', () => { it('calculates offset', () => { const content = "'foo:bar:1.2.3'"; const { deps } = parseGradle(content); const [res] = deps; const idx = content // TODO #22198 .slice(res.managerData!.fileReplacePosition) .indexOf('1.2.3'); expect(idx).toBe(0); }); it('parses fixture from "gradle" manager', () => { const content = Fixtures.get('build.gradle.example1'); const { deps } = parseGradle(content, {}, 'build.gradle'); const replacementIndices = deps.map(({ managerData, currentValue }) => // TODO #22198 content.slice(managerData!.fileReplacePosition).indexOf(currentValue!), ); expect(replacementIndices.every((idx) => idx === 0)).toBeTrue(); expect(deps).toMatchObject([ { currentValue: '1.5.2.RELEASE', depName: 'org.springframework.boot:spring-boot-gradle-plugin', sharedVariableName: 'springBootVersion', managerData: { fileReplacePosition: 53, packageFile: 'build.gradle', }, }, { currentValue: '1.2.3', depName: 'com.github.jengelman.gradle.plugins:shadow', managerData: { fileReplacePosition: 417, packageFile: 'build.gradle', }, }, { currentValue: '0.1', depName: 'com.fkorotkov:gradle-libraries-plugin', managerData: { fileReplacePosition: 481, packageFile: 'build.gradle', }, }, { currentValue: '0.2.3', depName: 'gradle.plugin.se.patrikerdes:gradle-use-latest-versions-plugin', managerData: { fileReplacePosition: 568, packageFile: 'build.gradle', }, }, { currentValue: '3.1.1', depName: 'org.apache.openjpa:openjpa', managerData: { fileReplacePosition: 621, packageFile: 'build.gradle', }, }, { currentValue: '0.13.0', depName: 'com.gradle.publish:plugin-publish-plugin', managerData: { fileReplacePosition: 688, packageFile: 'build.gradle', }, }, { currentValue: '6.0.9.RELEASE', depName: 'org.grails:gorm-hibernate5-spring-boot', managerData: { fileReplacePosition: 1882, packageFile: 'build.gradle', }, }, { currentValue: '6.0.5', depName: 'mysql:mysql-connector-java', managerData: { fileReplacePosition: 1938, packageFile: 'build.gradle', }, }, { currentValue: '1.0-groovy-2.4', depName: 'org.spockframework:spock-spring', managerData: { fileReplacePosition: 1996, packageFile: 'build.gradle', }, }, { currentValue: '1.3', depName: 'org.hamcrest:hamcrest-core', managerData: { fileReplacePosition: 2101, packageFile: 'build.gradle', }, }, { currentValue: '3.1', depName: 'cglib:cglib-nodep', managerData: { fileReplacePosition: 2189, packageFile: 'build.gradle', }, }, { currentValue: '3.1.1', depName: 'org.apache.openjpa:openjpa', managerData: { fileReplacePosition: 2295, packageFile: 'build.gradle', }, }, ]); }); }); describe('gradle.properties', () => { it.each` input | key | value | fileReplacePosition ${'foo=bar'} | ${'foo'} | ${'bar'} | ${4} ${' foo = bar '} | ${'foo'} | ${'bar'} | ${7} ${'foo.bar=baz'} | ${'foo.bar'} | ${'baz'} | ${8} ${'foo.bar:baz'} | ${'foo.bar'} | ${'baz'} | ${8} ${'foo.bar baz'} | ${'foo.bar'} | ${'baz'} | ${8} `('$input', ({ input, key, value, fileReplacePosition }) => { expect(parseProps(input)).toMatchObject({ vars: { [key]: { key, value, fileReplacePosition } }, }); }); it('handles multi-line file', () => { expect(parseProps('foo=foo\nbar=bar')).toMatchObject({ vars: { foo: { key: 'foo', value: 'foo', fileReplacePosition: 4 }, bar: { key: 'bar', value: 'bar', fileReplacePosition: 12 }, }, deps: [], }); }); it('attaches packageFile', () => { expect( parseProps('foo = bar', 'foo/bar/gradle.properties'), ).toMatchObject({ vars: { foo: { packageFile: 'foo/bar/gradle.properties' } }, }); }); it('parses dependencies', () => { const res = parseProps('dep = foo:bar:1.2.3'); expect(res).toMatchObject({ deps: [ { currentValue: '1.2.3', depName: 'foo:bar', managerData: { fileReplacePosition: 14 }, }, ], }); }); }); describe('apply from', () => { const key = 'version'; const value = '1.2.3'; const validOutput = { version: { key, value, fileReplacePosition: 11, packageFile: 'foo/bar.gradle', }, }; const fileContents = { 'foo/bar.gradle': `${key} = "${value}"`, }; it.each` def | input | output ${''} | ${'apply from: ""'} | ${{}} ${''} | ${'apply from: "foo/invalid.gradle"'} | ${{}} ${''} | ${'apply from: "${base}"'} | ${{}} ${''} | ${'apply from: "foo/invalid.non-gradle"'} | ${{}} ${''} | ${'apply from: "https://someurl.com/file.gradle"'} | ${{}} ${''} | ${'apply from: "foo/bar.gradle"'} | ${validOutput} ${'base="foo"'} | ${'apply from: "${base}/bar.gradle"'} | ${validOutput} ${'path="foo/bar.gradle"'} | ${'apply from: path'} | ${validOutput} ${'path="bar.gradle"'} | ${'apply from: "foo/" + path'} | ${validOutput} ${'path="foo/bar.gradle"'} | ${'apply from: property("path")'} | ${validOutput} ${''} | ${'apply from: file("foo/bar.gradle")'} | ${validOutput} ${'base="foo"'} | ${'apply from: file("${base}/bar.gradle")'} | ${validOutput} ${''} | ${'apply from: project.file("foo/bar.gradle")'} | ${validOutput} ${''} | ${'apply from: rootProject.file("foo/bar.gradle")'} | ${validOutput} ${''} | ${'apply from: new File("foo/bar.gradle")'} | ${validOutput} ${'base="foo"'} | ${'apply from: new File("${base}/bar.gradle")'} | ${validOutput} ${''} | ${'apply from: new File("foo", "bar.gradle")'} | ${validOutput} ${'base="foo"'} | ${'apply from: new File(base, "bar.gradle")'} | ${validOutput} ${'base="foo"'} | ${'apply from: new File("${base}", "bar.gradle")'} | ${validOutput} ${'path="bar.gradle"'} | ${'apply from: new File("foo", "${path}")'} | ${validOutput} ${'e="o"; b="gradle" '} | ${'apply from: new File("f" + e + e, "bar." + b)'} | ${validOutput} ${'a="bar"; b="gradle"'} | ${'apply from: new File("foo", a + "." + "${b}")'} | ${validOutput} ${'path="bar.gradle"'} | ${'apply from: new File("foo", property("path"))'} | ${validOutput} ${'base="foo"'} | ${'apply from: new File(property("base"), "bar.gradle")'} | ${validOutput} ${''} | ${'apply(from = "foo/bar.gradle"))'} | ${validOutput} ${'base="foo"'} | ${'apply(from = "${base}/bar.gradle"))'} | ${validOutput} ${''} | ${'apply(from = File("foo/bar.gradle"))'} | ${validOutput} ${''} | ${'apply(from = File("foo", "bar", "baz"))'} | ${{}} ${''} | ${'apply(from = File(["${somedir}/foo.gradle"]))'} | ${{}} ${'base="foo"'} | ${'apply(from = File("${base}/bar.gradle"))'} | ${validOutput} ${''} | ${'apply(from = File("foo", "bar.gradle"))'} | ${validOutput} ${'base="foo"'} | ${'apply(from = File(base, "bar.gradle"))'} | ${validOutput} ${'base="foo"'} | ${'apply(from = File("${base}", "bar.gradle"))'} | ${validOutput} `('$def | $input', ({ def, input, output }) => { mockFs(); const { vars } = parseGradle( [def, input].join('\n'), {}, '', fileContents, ); expect(vars).toMatchObject(output); }); it('recursion check', () => { const { vars } = parseGradle( 'apply from: "foo/bar.gradle"', {}, '', fileContents, 3, ); expect(logger.logger.debug).toHaveBeenCalledWith( 'Max recursion depth reached in script file: foo/bar.gradle', ); expect(vars).toBeEmpty(); }); }); describe('implicit gradle plugins', () => { it.each` def | input | output ${'baz = "1.2.3"'} | ${'checkstyle { toolVersion = "${baz}" }'} | ${{ depName: 'checkstyle', packageName: GRADLE_PLUGINS.checkstyle[1], currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'checkstyle { toolVersion "${baz}" }'} | ${{ depName: 'checkstyle', packageName: GRADLE_PLUGINS.checkstyle[1], currentValue: '1.2.3' }} ${''} | ${'codenarc { toolVersion = "1.2.3" }'} | ${{ depName: 'codenarc', packageName: GRADLE_PLUGINS.codenarc[1], currentValue: '1.2.3' }} ${''} | ${'detekt { toolVersion = "1.2.3" }'} | ${{ depName: 'detekt', packageName: GRADLE_PLUGINS.detekt[1], currentValue: '1.2.3' }} ${''} | ${'findbugs { toolVersion = "1.2.3" }'} | ${{ depName: 'findbugs', packageName: GRADLE_PLUGINS.findbugs[1], currentValue: '1.2.3' }} ${''} | ${'googleJavaFormat { toolVersion = "1.2.3" }'} | ${{ depName: 'googleJavaFormat', packageName: GRADLE_PLUGINS.googleJavaFormat[1], currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'jacoco { toolVersion = baz }'} | ${{ depName: 'jacoco', packageName: GRADLE_PLUGINS.jacoco[1], currentValue: '1.2.3', sharedVariableName: 'baz' }} ${'baz = "1.2.3"'} | ${'jacoco { toolVersion = property("baz") }'} | ${{ depName: 'jacoco', packageName: GRADLE_PLUGINS.jacoco[1], currentValue: '1.2.3' }} ${''} | ${'lombok { version = "1.2.3" }'} | ${{ depName: 'lombok', packageName: GRADLE_PLUGINS.lombok[1], currentValue: '1.2.3' }} ${''} | ${'lombok { version.set("1.2.3") }'} | ${{ depName: 'lombok', packageName: GRADLE_PLUGINS.lombok[1], currentValue: '1.2.3' }} ${''} | ${'lombok { version.value("1.2.3") }'} | ${{ depName: 'lombok', packageName: GRADLE_PLUGINS.lombok[1], currentValue: '1.2.3' }} ${''} | ${'pmd { toolVersion = "1.2.3" }'} | ${{ depName: 'pmd', packageName: GRADLE_PLUGINS.pmd[1], currentValue: '1.2.3' }} ${''} | ${'pmd { toolVersion.set("1.2.3") }'} | ${{ depName: 'pmd', packageName: GRADLE_PLUGINS.pmd[1], currentValue: '1.2.3' }} ${''} | ${'pmd { toolVersion.value("1.2.3") }'} | ${{ depName: 'pmd', packageName: GRADLE_PLUGINS.pmd[1], currentValue: '1.2.3' }} ${''} | ${'pmd { foo = "bar"; toolVersion = "1.2.3" }'} | ${{ depName: 'pmd', packageName: GRADLE_PLUGINS.pmd[1], currentValue: '1.2.3' }} ${''} | ${'spotbugs { toolVersion = "1.2.3" }'} | ${{ depName: 'spotbugs', packageName: GRADLE_PLUGINS.spotbugs[1], currentValue: '1.2.3' }} ${''} | ${'pmd { toolVersion = "@@@" }'} | ${null} ${''} | ${'pmd { toolVersion = "${baz}" }'} | ${null} ${'baz = "1.2.3"'} | ${'pmd { toolVersion = "${baz}.456" }'} | ${{ depName: 'pmd', currentValue: '1.2.3.456', skipReason: 'unspecified-version' }} ${'baz = "1.2.3"'} | ${'pmd { toolVersion = baz + ".456" }'} | ${{ depName: 'pmd', currentValue: '1.2.3.456', skipReason: 'unspecified-version' }} ${''} | ${'pmd { [toolVersion = "6.36.0"] }'} | ${null} ${''} | ${'unknown { toolVersion = "1.2.3" }'} | ${null} ${''} | ${'composeOptions { kotlinCompilerExtensionVersion = "1.2.3" }'} | ${{ depName: 'composeOptions', packageName: GRADLE_PLUGINS.composeOptions[1], currentValue: '1.2.3' }} ${''} | ${'jmh { jmhVersion = "1.2.3" }'} | ${{ depName: 'jmh', packageName: GRADLE_PLUGINS.jmh[1], currentValue: '1.2.3' }} ${''} | ${'micronaut { version = "1.2.3" }'} | ${{ depName: 'micronaut', packageName: GRADLE_PLUGINS.micronaut[1], currentValue: '1.2.3' }} `('$def | $input', ({ def, input, output }) => { const { deps } = parseGradle([def, input].join('\n')); expect(deps).toMatchObject([output].filter(isTruthy)); }); }); describe('implicit gradle test suite dependencies', () => { it.each` def | input | output ${''} | ${'testing.suites.test { useJunit("1.2.3") } } }'} | ${{ depName: 'useJunit', packageName: GRADLE_TEST_SUITES.useJunit, currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'testing.suites.test { useJUnitJupiter(baz) } } }'} | ${{ depName: 'useJUnitJupiter', packageName: GRADLE_TEST_SUITES.useJUnitJupiter, currentValue: '1.2.3' }} ${''} | ${'testing.suites.test { useKotlinTest("1.2.3") } } }'} | ${{ depName: 'useKotlinTest', packageName: GRADLE_TEST_SUITES.useKotlinTest, currentValue: '1.2.3' }} ${'baz = "1.2.3"'} | ${'testing { suites { test { useSpock(baz) } } }'} | ${{ depName: 'useSpock', packageName: GRADLE_TEST_SUITES.useSpock, currentValue: '1.2.3' }} ${''} | ${'testing.suites.test { useSpock("1.2.3") } } }'} | ${{ depName: 'useSpock', packageName: GRADLE_TEST_SUITES.useSpock, currentValue: '1.2.3' }} ${''} | ${'testing.suites.test { useTestNG("1.2.3") } } }'} | ${{ depName: 'useTestNG', packageName: GRADLE_TEST_SUITES.useTestNG, currentValue: '1.2.3' }} ${''} | ${'testing { suites.withType(JvmTestSuite).configureEach { useJUnitJupiter("5.13.4") } }'} | ${{ depName: 'useJUnitJupiter', packageName: GRADLE_TEST_SUITES.useJUnitJupiter, currentValue: '5.13.4' }} ${''} | ${'testing { suites.withType(JvmTestSuite::class).configureEach { useSpock("2.3") } }'} | ${{ depName: 'useSpock', packageName: GRADLE_TEST_SUITES.useSpock, currentValue: '2.3' }} `('$def | $input', ({ def, input, output }) => { const { deps } = parseGradle([def, input].join('\n')); expect(deps).toMatchObject([output]); }); }); 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 deps2 = listOf( "androidx.appcompat:appcompat:4.5.6", "androidx.core:core-ktx:\${Versions.baz}", listOf("androidx.webkit:webkit:\${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', sharedVariableName: 'Versions.baz', currentValue: '1.2.3', }, { depName: 'androidx.appcompat:appcompat', currentValue: '4.5.6', }, { depName: 'androidx.core:core-ktx', sharedVariableName: 'Versions.baz', currentValue: '1.2.3', }, { depName: 'androidx.webkit:webkit', sharedVariableName: 'Versions.baz', currentValue: '1.2.3', }, { depName: 'foo:bar', sharedVariableName: '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 { object Release { 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.Release.version': { key: 'Deps.Test.Espresso.Release.version', value: '3.3.0-rc01', }, }, deps: [ { depName: 'org.jetbrains.kotlin:kotlin-stdlib-jdk7', currentValue: '1.5.31', sharedVariableName: 'Deps.kotlinVersion', }, { depName: 'androidx.test:core', currentValue: '1.3.0-rc01', sharedVariableName: 'Deps.Test.version', }, { depName: 'androidx.test.espresso:espresso-core', currentValue: '3.3.0-rc01', sharedVariableName: 'Deps.Test.Espresso.Release.version', }, { depName: 'androidx.test:core-ktx', currentValue: '1.3.0-rc01', sharedVariableName: 'Deps.Test.version', }, ], }); }); it('imported objects', () => { const input = codeBlock` object ModuleConfiguration { object Build { object Database { const val h2Version = "2.0.206" } } } `; const gradleKtsInput = codeBlock` import ModuleConfiguration.Build.Database dependencies { runtimeOnly("com.h2database:h2:\${Database.h2Version}") } `; const { vars } = parseKotlinSource(input); const res = parseGradle(gradleKtsInput, vars); expect(res).toMatchObject({ vars: { 'ModuleConfiguration.Build.Database.h2Version': { key: 'ModuleConfiguration.Build.Database.h2Version', value: '2.0.206', }, }, deps: [ { depName: 'com.h2database:h2', currentValue: '2.0.206', sharedVariableName: 'ModuleConfiguration.Build.Database.h2Version', }, ], }); }); }); describe('Java language version', () => { it.each` input | output ${'java { toolchain { languageVersion = JavaLanguageVersion.of(22) } }'} | ${'22'} ${'java { toolchain.languageVersion.set(JavaLanguageVersion.of(16)) }'} | ${'16'} ${'java.toolchain { languageVersion.set(JavaLanguageVersion.of(17)) }'} | ${'17'} ${'java.toolchain.languageVersion = JavaLanguageVersion.of(21)'} | ${'21'} ${'kotlin { jvmToolchain { languageVersion = JavaLanguageVersion.of(17) } }'} | ${'17'} ${'kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) } }'} | ${'17'} ${'kotlin.jvmToolchain { languageVersion.set(JavaLanguageVersion.of(8)) }'} | ${'8'} ${'kotlin { jvmToolchain(11) }'} | ${'11'} ${'kotlin.jvmToolchain(16)'} | ${'16'} ${'dependencies { implementation "com.google.protobuf:protobuf-java:2.17.0" }'} | ${null} `('$input', ({ input, output }) => { const javaLanguageVersion = parseJavaToolchainVersion(input); expect(javaLanguageVersion).toBe(output); }); }); });