import { codeBlock } from 'common-tags'; import { Fixtures } from '~test/fixtures.ts'; import { fs, logger, partial } from '~test/util.ts'; import type { ExtractConfig, PackageDependency } from '../types.ts'; import { matchesContentDescriptor } from './extract.ts'; import { extractAllPackageFiles } from './index.ts'; import * as parser from './parser.ts'; vi.mock('../../../util/fs/index.ts'); function mockFs(files: Record): void { fs.getLocalFiles.mockImplementation( (fileNames: string[]): Promise> => { const fileContentMap: Record = {}; for (const fileName of fileNames) { fileContentMap[fileName] = files?.[fileName]; } return Promise.resolve(fileContentMap); }, ); fs.getSiblingFileName.mockImplementation( (existingFileNameWithPath: string, otherFileName: string) => { return existingFileNameWithPath .slice(0, existingFileNameWithPath.lastIndexOf('/') + 1) .concat(otherFileName); }, ); } describe('modules/manager/gradle/extract', () => { beforeEach(() => { vi.restoreAllMocks(); }); it('returns null', async () => { const fsMock = { 'gradle.properties': '', 'build.gradle': '', }; mockFs(fsMock); await expect( extractAllPackageFiles(partial(), Object.keys(fsMock)), ).resolves.toBeNull(); }); it('logs a warning in case parseGradle throws an exception', async () => { const filename = 'build.gradle'; const err = new Error('unknown'); const fsMock = { 'build.gradle': '', }; mockFs(fsMock); vi.spyOn(parser, 'parseGradle').mockImplementationOnce(() => { throw err; }); await extractAllPackageFiles(partial(), [filename]); expect(logger.logger.debug).toHaveBeenCalledWith( { err, config: {}, packageFile: filename }, `Failed to process Gradle file`, ); }); it('skips versions composed from multiple variables', async () => { const fsMock = { 'build.gradle': 'foo = "1"; bar = "2"; baz = "3"; "foo:bar:$foo.$bar.$baz"', }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'build.gradle', deps: [ { depName: 'foo:bar', currentValue: '1.2.3', skipReason: 'contains-variable', }, ], }, ]); }); it('extracts from cross-referenced files', async () => { const fsMock = { 'gradle.properties': 'baz=1.2.3', 'build.gradle': 'repositories { maven { url "https://example.com" } }; "foo:bar:$baz@zip"', }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle.properties', deps: [ { depName: 'foo:bar', currentValue: '1.2.3', registryUrls: ['https://example.com'], }, ], }, { packageFile: 'build.gradle', deps: [] }, ]); }); it('resolves versions in build.gradle.kts', async () => { const fsMock = { 'build.gradle.kts': codeBlock` object Versions { const val RETROFIT = "2.9.0" const val SPRING_BOOT = "3.5.5" const val DETEKT_FORMATTING = "1.23.6" const val SPRING_CLOUD = "4.2.1" } allprojects { dependencies { constraints { api("com.squareup.retrofit2:retrofit:\${Versions.RETROFIT}") } kapt(platform("org.springframework.boot:spring-boot-dependencies:\${Versions.SPRING_BOOT}")) detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:\${Versions.DETEKT_FORMATTING}") implementation( platform( "org.springframework.cloud:spring-cloud-contract-dependencies:" + Versions.SPRING_CLOUD ) ) implementation(platform("org.springframework.boot:spring-boot-dependencies:\${Versions.SPRING_BOOT}")) } } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'build.gradle.kts', deps: [ { depName: 'com.squareup.retrofit2:retrofit', currentValue: '2.9.0', sharedVariableName: 'Versions.RETROFIT', }, { depName: 'org.springframework.boot:spring-boot-dependencies', currentValue: '3.5.5', sharedVariableName: 'Versions.SPRING_BOOT', }, { depName: 'io.gitlab.arturbosch.detekt:detekt-formatting', currentValue: '1.23.6', sharedVariableName: 'Versions.DETEKT_FORMATTING', }, { depName: 'org.springframework.cloud:spring-cloud-contract-dependencies', currentValue: '4.2.1', sharedVariableName: 'Versions.SPRING_CLOUD', }, ], }, ]); }); 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(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'buildSrc/src/main/kotlin/Deps.kt', deps: [ { depName: 'javax.cache:cache-api', currentValue: '1.1.0', sharedVariableName: 'Libraries.jCache', }, { depName: 'com.android.tools.build:gradle', currentValue: '4.1.2', sharedVariableName: 'Libraries.Android.Tools.version', }, { depName: 'androidx.test:core', currentValue: '1.3.0-rc01', sharedVariableName: 'Libraries.Test.version', }, { depName: 'androidx.test.espresso:espresso-core', currentValue: '3.3.0-rc01', sharedVariableName: 'Libraries.Test.Espresso.version', }, { depName: 'androidx.test:core-ktx', currentValue: '1.3.0-rc01', sharedVariableName: 'Libraries.Test.version', }, ], }, { packageFile: 'buildSrc/src/main/kotlin/GradleDeps.kt', deps: [ { depName: 'org.jetbrains.kotlin:kotlin-stdlib', currentValue: '1.8.10', sharedVariableName: 'GradleDeps.Kotlin.version', }, ], }, { packageFile: 'buildSrc/src/main/kotlin/Versions.kt', deps: [ { depName: 'com.fasterxml.jackson.core:jackson-annotations', currentValue: '2.9.10', sharedVariableName: 'Versions.jackson', }, { depName: 'io.reactivex.rxjava2:rxjava', currentValue: '1.2.3', sharedVariableName: 'Versions.rxjava', }, ], }, ]); }); it('inherits gradle variables', async () => { const fsMock = { 'gradle.properties': 'foo=1.0.0', 'build.gradle': 'foo = "1.0.1"', 'aaa/gradle.properties': 'bar = "2.0.0"', 'aaa/build.gradle': 'bar = "2.0.1"', 'aaa/bbb/build.gradle': '"foo:foo:$foo"; "bar:bar:$bar"', }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle.properties', deps: [] }, { packageFile: 'build.gradle', deps: [{ depName: 'foo:foo', currentValue: '1.0.1' }], }, { packageFile: 'aaa/gradle.properties', deps: [] }, { packageFile: 'aaa/build.gradle', deps: [{ depName: 'bar:bar', currentValue: '2.0.1' }], }, { packageFile: 'aaa/bbb/build.gradle', deps: [] }, ]); }); it('filters duplicate dependency findings', async () => { const buildFile = codeBlock` apply from: 'test.gradle' repositories { mavenCentral() } dependencies { implementation "io.jsonwebtoken:jjwt-api:$\{jjwtVersion}" runtimeOnly "io.jsonwebtoken:jjwt-impl:$\{jjwtVersion}" } `; const testFile = codeBlock` ext.jjwtVersion = '0.11.2' ext { jjwtApi = "io.jsonwebtoken:jjwt-api:$jjwtVersion" } `; const fsMock = { 'build.gradle': buildFile, 'test.gradle': testFile, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'test.gradle', deps: [ { depName: 'io.jsonwebtoken:jjwt-api' }, { depName: 'io.jsonwebtoken:jjwt-impl' }, ], }, { packageFile: 'build.gradle' }, ]); }); it('ensures depType is assigned', async () => { const fsMock = { 'build.gradle': "id 'org.sonarqube' version '3.1.1'\n\"io.jsonwebtoken:jjwt-api:0.11.2\"", 'buildSrc/build.gradle': '"com.google.protobuf:protobuf-java:3.18.2"', }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'build.gradle', deps: [ { depName: 'org.sonarqube', depType: 'plugin' }, { depName: 'io.jsonwebtoken:jjwt-api', depType: 'dependencies' }, ], }, { packageFile: 'buildSrc/build.gradle', deps: [{ depType: 'devDependencies' }], }, ]); }); describe('registry URLs', () => { it('deduplicates registry urls', async () => { const fsMock = { 'build.gradle': codeBlock` repositories { maven { url "https://repo.maven.apache.org/maven2" } } repositories { maven { url "https://repo.maven.apache.org/maven2" } } repositories { maven { url "https://example.com" } } repositories { maven { url "https://example.com" } } plugins { id "foo.bar" version "1.2.3" } dependencies { classpath "foo:bar:1.2.3" } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'build.gradle', deps: [ { depType: 'plugin', registryUrls: ['https://plugins.gradle.org/m2/'], }, { registryUrls: [ 'https://repo.maven.apache.org/maven2', 'https://example.com', ], }, ], }, ]); }); it('interpolates registry URLs', async () => { const buildFile = codeBlock` repositories { mavenCentral() maven { url = "\${repositoryBaseURL}/repository-build" } maven { name = "baz" url = "\${repositoryBaseURL}/\${name}" } } dependencies { implementation "com.google.protobuf:protobuf-java:2.17.0" } `; const fsMock = { 'build.gradle': buildFile, 'gradle.properties': 'repositoryBaseURL: https\\://dummy.org/whatever', }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle.properties', deps: [], }, { packageFile: 'build.gradle', deps: [ { depName: 'com.google.protobuf:protobuf-java', currentValue: '2.17.0', managerData: { fileReplacePosition: 262, packageFile: 'build.gradle', }, fileReplacePosition: 262, registryUrls: [ 'https://repo.maven.apache.org/maven2', 'https://dummy.org/whatever/repository-build', 'https://dummy.org/whatever/baz', ], }, ], }, ]); }); it('supports separate registry URLs for plugins', async () => { const settingsFile = codeBlock` pluginManagement { repositories { mavenLocal() maven { url = "https://foo.bar/plugins" } } } `; const buildFile = codeBlock` plugins { id "foo.bar" version "1.2.3" } repositories { maven { url = "https://foo.bar/deps" } mavenCentral() } dependencies { classpath "io.jsonwebtoken:jjwt-api:0.11.2" } `; const fsMock = { 'build.gradle': buildFile, 'settings.gradle': settingsFile, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'settings.gradle', deps: [], }, { packageFile: 'build.gradle', deps: [ { depName: 'foo.bar', depType: 'plugin', registryUrls: ['https://foo.bar/plugins'], }, { depName: 'io.jsonwebtoken:jjwt-api', registryUrls: [ 'https://foo.bar/deps', 'https://repo.maven.apache.org/maven2', ], }, ], }, ]); }); describe('content descriptors', () => { describe('simple descriptor matches', () => { it.each` input | output | descriptor ${'foo:bar:1.2.3'} | ${true} | ${undefined} ${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo' }]} ${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo' }]} ${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'bar' }]} ${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar' }]} ${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar' }]} ${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'baz' }]} ${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3' }]} ${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.3' }]} ${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'bar', version: '1.2.+' }]} ${'foo:bar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'simple', groupId: 'foo', artifactId: 'baz', version: '4.5.6' }]} ${'foo:bar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo' }]} ${'foo.bar.baz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.bar.baz' }]} ${'foo.bar.baz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.bar' }]} ${'foo.bar.baz:qux:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'subgroup', groupId: 'foo.barbaz' }]} ${'foobarbaz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*' }]} ${'foobarbaz:qux:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*', artifactId: 'qux' }]} ${'foobar:foobar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: '.*bar.*', artifactId: 'foo.*' }]} ${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^bar' }]} ${'foobar:foobar:1.2.3'} | ${true} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^foo.*', version: '1\\.*' }]} ${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: '^foo', version: '3.+' }]} ${'foobar:foobar:1.2.3'} | ${false} | ${[{ mode: 'include', matcher: 'regex', groupId: 'foobar', artifactId: 'qux', version: '1\\.*' }]} `('$input | $output', ({ input, output, descriptor }) => { const [groupId, artifactId, currentValue] = input.split(':'); const dep: PackageDependency = { depName: `${groupId}:${artifactId}`, currentValue, }; expect(matchesContentDescriptor(dep, descriptor)).toBe(output); }); }); describe('multiple descriptors', () => { const dep: PackageDependency = { depName: `foo:bar`, currentValue: '1.2.3', }; it('if both includes and excludes exist, dep must match include and not match exclude', () => { expect( matchesContentDescriptor(dep, [ { mode: 'include', matcher: 'simple', groupId: 'foo' }, { mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'baz', }, ]), ).toBe(true); expect( matchesContentDescriptor(dep, [ { mode: 'include', matcher: 'simple', groupId: 'foo' }, { mode: 'exclude', matcher: 'simple', groupId: 'foo', artifactId: 'bar', }, ]), ).toBe(false); }); it('if only includes exist, dep must match at least one include', () => { expect( matchesContentDescriptor(dep, [ { mode: 'include', matcher: 'simple', groupId: 'some' }, { mode: 'include', matcher: 'simple', groupId: 'foo' }, { mode: 'include', matcher: 'simple', groupId: 'bar' }, ]), ).toBe(true); expect( matchesContentDescriptor(dep, [ { mode: 'include', matcher: 'simple', groupId: 'some' }, { mode: 'include', matcher: 'simple', groupId: 'other' }, { mode: 'include', matcher: 'simple', groupId: 'bar' }, ]), ).toBe(false); }); it('if only excludes exist, dep must match not match any exclude', () => { expect( matchesContentDescriptor(dep, [ { mode: 'exclude', matcher: 'simple', groupId: 'some' }, { mode: 'exclude', matcher: 'simple', groupId: 'foo' }, { mode: 'exclude', matcher: 'simple', groupId: 'bar' }, ]), ).toBe(false); expect( matchesContentDescriptor(dep, [ { mode: 'exclude', matcher: 'simple', groupId: 'some' }, { mode: 'exclude', matcher: 'simple', groupId: 'other' }, { mode: 'exclude', matcher: 'simple', groupId: 'bar' }, ]), ).toBe(true); }); }); it('extracts content descriptors', async () => { const fsMock = { 'build.gradle': codeBlock` pluginManagement { repositories { maven { url = "https://foo.bar/baz" content { includeModule("com.diffplug.spotless", "com.diffplug.spotless.gradle.plugin") } } } } repositories { mavenCentral() google { content { includeGroupAndSubgroups("foo.bar") includeModuleByRegex("com\\\\.(google|android).*", "protobuf.*") includeGroupByRegex("(?!(unsupported|pattern).*)") includeGroupByRegex "org\\\\.jetbrains\\\\.kotlin.*" excludeModule("foo.bar.group", "simple.module") } } maven { name = "some" url = "https://foo.bar/\${name}" content { includeModule("foo.bar.group", "simple.module") includeVersion("com.google.protobuf", "protobuf-java", "2.17.+") } } } plugins { id("com.diffplug.spotless") version "6.10.0" } dependencies { implementation "com.google.protobuf:protobuf-java:2.17.1" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.21" implementation "foo.bar:protobuf-java:2.17.0" implementation "foo.bar.group:simple.module:2.17.0" } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { deps: [ { depName: 'com.diffplug.spotless', currentValue: '6.10.0', depType: 'plugin', packageName: 'com.diffplug.spotless:com.diffplug.spotless.gradle.plugin', registryUrls: ['https://foo.bar/baz'], }, { depName: 'com.google.protobuf:protobuf-java', currentValue: '2.17.1', registryUrls: [ 'https://repo.maven.apache.org/maven2', 'https://dl.google.com/android/maven2/', 'https://foo.bar/some', ], }, { depName: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8', currentValue: '1.4.21', registryUrls: [ 'https://repo.maven.apache.org/maven2', 'https://dl.google.com/android/maven2/', ], }, { depName: 'foo.bar:protobuf-java', currentValue: '2.17.0', registryUrls: [ 'https://repo.maven.apache.org/maven2', 'https://dl.google.com/android/maven2/', ], }, { depName: 'foo.bar.group:simple.module', currentValue: '2.17.0', registryUrls: [ 'https://repo.maven.apache.org/maven2', 'https://foo.bar/some', ], }, ], }, ]); }); }); it('exclusiveContent', async () => { const fsMock = { 'build.gradle': codeBlock` repositories { google() exclusiveContent { forRepository { maven { url "https://artifactory.foo.bar/artifactory/test" } } filter { includeGroup "foo.bar" } } } dependencies { implementation "com.google.protobuf:protobuf-java:2.17.1" implementation "foo.bar:protobuf-java:2.17.0" } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { deps: [ { depName: 'com.google.protobuf:protobuf-java', currentValue: '2.17.1', registryUrls: ['https://dl.google.com/android/maven2/'], }, { depName: 'foo.bar:protobuf-java', currentValue: '2.17.0', registryUrls: ['https://artifactory.foo.bar/artifactory/test'], }, ], }, ]); }); it('exclusiveContent with repeated repository definition', async () => { const fsMock = { 'build.gradle': codeBlock` repositories { google() exclusiveContent { forRepository { maven { url "https://artifactory.foo.bar/artifactory/test" } } filter { includeGroup "some.dll" } } exclusiveContent { forRepository { maven { url "https://artifactory.foo.bar/artifactory/test" } } filter { includeGroup "foo.bar" } } } dependencies { implementation "com.google.protobuf:protobuf-java:2.17.1" implementation "foo.bar:protobuf-java:2.17.0" implementation "some.dll:dll-1:1.0.0" } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { deps: [ { depName: 'com.google.protobuf:protobuf-java', currentValue: '2.17.1', registryUrls: ['https://dl.google.com/android/maven2/'], }, { depName: 'foo.bar:protobuf-java', currentValue: '2.17.0', registryUrls: ['https://artifactory.foo.bar/artifactory/test'], }, { depName: 'some.dll:dll-1', currentValue: '1.0.0', registryUrls: ['https://artifactory.foo.bar/artifactory/test'], }, ], }, ]); }); }); describe('version catalogs', () => { it('works with dependency catalogs', async () => { const fsMock = { 'gradle/libs.versions.toml': Fixtures.get('libs.versions.toml'), }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle/libs.versions.toml', deps: [ { depName: 'io.gitlab.arturbosch.detekt:detekt-formatting', sharedVariableName: 'detekt', currentValue: '1.17.0', managerData: { fileReplacePosition: 21, packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'io.kotest:kotest-assertions-core-jvm', sharedVariableName: 'kotest', currentValue: '4.6.0', managerData: { fileReplacePosition: 51, packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'io.kotest:kotest-runner-junit5', sharedVariableName: 'kotest', currentValue: '4.6.0', managerData: { fileReplacePosition: 51, packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'org.mockito:mockito-core', currentValue: '3.10.0', managerData: { fileReplacePosition: 474, packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'com.github.siom79.japicmp:japicmp', currentValue: '0.15.+', managerData: { fileReplacePosition: 561, packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'guava', skipReason: 'multiple-constraint-dep', managerData: { packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'gson', skipReason: 'unsupported-version', managerData: { packageFile: 'gradle/libs.versions.toml', }, }, { depName: 'io.gitlab.arturbosch.detekt', depType: 'plugin', currentValue: '1.17.0', packageName: 'io.gitlab.arturbosch.detekt:io.gitlab.arturbosch.detekt.gradle.plugin', managerData: { fileReplacePosition: 21, packageFile: 'gradle/libs.versions.toml', }, registryUrls: ['https://plugins.gradle.org/m2/'], }, { depName: 'org.danilopianini.publish-on-central', depType: 'plugin', currentValue: '0.5.0', packageName: 'org.danilopianini.publish-on-central:org.danilopianini.publish-on-central.gradle.plugin', managerData: { fileReplacePosition: 82, packageFile: 'gradle/libs.versions.toml', }, registryUrls: ['https://plugins.gradle.org/m2/'], }, { depName: 'org.ajoberstar.grgit', depType: 'plugin', commitMessageTopic: 'plugin grgit', packageName: 'org.ajoberstar.grgit:org.ajoberstar.grgit.gradle.plugin', managerData: { packageFile: 'gradle/libs.versions.toml', }, registryUrls: ['https://plugins.gradle.org/m2/'], skipReason: 'unspecified-version', }, { depName: 'org.ajoberstar.grgit2', depType: 'plugin', skipReason: 'unspecified-version', }, ], }, ]); }); it('provides versions from external version catalogs to gradle files', async () => { const fsMock = { 'gradle/libs.versions.toml': codeBlock` [versions] detekt = "1.18.1" kotlin = "2.2.20" springBoot = "3.5.4" `, 'build.gradle.kts': codeBlock` plugins { id "org.springframework.boot" version libs.versions.springBoot } detekt { toolVersion = libs.versions.detekt.get() } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:\${libs.versions.kotlin.get()}") } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle/libs.versions.toml', deps: [ { packageName: 'org.springframework.boot:org.springframework.boot.gradle.plugin', currentValue: '3.5.4', sharedVariableName: 'libs.versions.springBoot', }, { packageName: 'io.gitlab.arturbosch.detekt:detekt-core', currentValue: '1.18.1', sharedVariableName: 'libs.versions.detekt', }, { depName: 'org.jetbrains.kotlin:kotlin-gradle-plugin', currentValue: '2.2.20', sharedVariableName: 'libs.versions.kotlin', }, ], }, { packageFile: 'build.gradle.kts', deps: [], }, ]); }); it('provides versions to gradle files with changed default catalog name', async () => { const fsMock = { 'gradle/libs.versions.toml': codeBlock` [versions] kotlin = "2.2.20" `, 'settings.gradle.kts': codeBlock` dependencyResolutionManagement { defaultLibrariesExtensionName = "projectLibs" } `, 'build.gradle.kts': codeBlock` dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:\${projectLibs.versions.kotlin.get()}") } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'settings.gradle.kts', deps: [], }, { packageFile: 'gradle/libs.versions.toml', deps: [ { depName: 'org.jetbrains.kotlin:kotlin-gradle-plugin', currentValue: '2.2.20', sharedVariableName: 'projectLibs.versions.kotlin', }, ], }, { packageFile: 'build.gradle.kts', deps: [], }, ]); }); it('ignores version catalog accessor with non-get provider method', async () => { const fsMock = { 'gradle/libs.versions.toml': codeBlock` [versions] detekt = "1.18.1" `, 'build.gradle.kts': codeBlock` detekt { toolVersion = libs.versions.detekt.getOrNull() } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toBeNull(); }); it('aligns sharedVariableName if version reference has multiple aliases', async () => { const fsMock = { 'gradle/libs.versions.toml': codeBlock` [versions] android-test = "1.0.2" [libraries] android-test-runner = { group = "com.android.support.test", name = "runner", version.ref = "android-test" } `, 'build.gradle.kts': codeBlock` dependencies { classpath("com.android.support.test:rules:\${libs.versions.android.test.get()}") } `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle/libs.versions.toml', deps: [ { depName: 'com.android.support.test:runner', currentValue: '1.0.2', fileReplacePosition: 27, sharedVariableName: 'android.test', }, { depName: 'com.android.support.test:rules', currentValue: '1.0.2', fileReplacePosition: 27, sharedVariableName: 'android.test', }, ], }, { packageFile: 'build.gradle.kts', deps: [], }, ]); }); }); describe('apply from', () => { it('loads further scripts using apply from statements', async () => { const buildFile = codeBlock` buildscript { repositories { mavenCentral() } apply from: "\${someDir}/libs1.gradle" apply from: file("gradle/libs2.gradle") apply from: "gradle/libs3.gradle" apply from: new File(someDir, "\${someDir}/libs4.gradle") apply from: file("gradle/non-existing.gradle") dependencies { classpath "com.google.protobuf:protobuf-java:\${protoBufVersion}" classpath "com.google.guava:guava:\${guavaVersion}" classpath "io.jsonwebtoken:jjwt-api:0.11.2" classpath "org.junit.jupiter:junit-jupiter-api:\${junitVersion}" classpath "org.junit.jupiter:junit-jupiter-engine:\${junitVersion}" classpath "org.slf4j:slf4j-api:\${slf4jVersion}" } } `; const fsMock = { 'gradleX/libs1.gradle': "ext.junitVersion = '5.5.2'", 'gradle/libs2.gradle': "ext.protoBufVersion = '3.18.2'", 'gradle/libs3.gradle': "ext.guavaVersion = '30.1-jre'", 'gradleX/gradleX/libs4.gradle': "ext.slf4jVersion = '1.7.30'", 'build.gradle': buildFile, 'gradle.properties': 'someDir=gradleX', }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'gradle.properties' }, { packageFile: 'build.gradle', deps: [{ depName: 'io.jsonwebtoken:jjwt-api' }], }, { packageFile: 'gradle/libs2.gradle', deps: [ { depName: 'com.google.protobuf:protobuf-java', currentValue: '3.18.2', managerData: { packageFile: 'gradle/libs2.gradle' }, }, ], }, { packageFile: 'gradle/libs3.gradle', deps: [ { depName: 'com.google.guava:guava', currentValue: '30.1-jre', managerData: { packageFile: 'gradle/libs3.gradle' }, }, ], }, { packageFile: 'gradleX/libs1.gradle', deps: [ { depName: 'org.junit.jupiter:junit-jupiter-api', currentValue: '5.5.2', managerData: { packageFile: 'gradleX/libs1.gradle' }, }, { depName: 'org.junit.jupiter:junit-jupiter-engine', currentValue: '5.5.2', managerData: { packageFile: 'gradleX/libs1.gradle' }, }, ], }, { packageFile: 'gradleX/gradleX/libs4.gradle', deps: [ { depName: 'org.slf4j:slf4j-api', currentValue: '1.7.30', managerData: { packageFile: 'gradleX/gradleX/libs4.gradle' }, }, ], }, ]); }); it('works with files in sub-directories', async () => { const buildFile = ` buildscript { repositories { mavenCentral() } apply from: "gradle/libs4.gradle" dependencies { classpath "com.google.protobuf:protobuf-java:\${protoBufVersion}" } } `; const fsMock = { 'somesubdir/gradle/libs4.gradle': "ext.protoBufVersion = '3.18.2'", 'somesubdir/build.gradle': buildFile, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'somesubdir/build.gradle' }, { packageFile: 'somesubdir/gradle/libs4.gradle', deps: [{ depName: 'com.google.protobuf:protobuf-java' }], }, ]); }); it('prevents recursive apply from calls', async () => { const fsMock = { 'build.gradle': "apply from: 'test.gradle'", 'test.gradle': "apply from: 'build.gradle'", }; mockFs(fsMock); await expect( extractAllPackageFiles(partial(), Object.keys(fsMock)), ).resolves.toBeNull(); }); it('prevents inclusion of non-Gradle files', async () => { const fsMock = { 'build.gradle': "apply from: '~test.non-gradle'", }; mockFs(fsMock); await expect( extractAllPackageFiles(partial(), Object.keys(fsMock)), ).resolves.toBeNull(); }); }); describe('gradle-consistent-versions plugin', () => { it('parses versions files', async () => { const fsMock = { 'versions.props': `org.apache.lucene:* = 1.2.3`, 'versions.lock': codeBlock` # Run ./gradlew --write-locks to regenerate this file org.apache.lucene:lucene-core:1.2.3 (10 constraints: 95be0c15) org.apache.lucene:lucene-codecs:1.2.3 (5 constraints: 1231231) `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toMatchObject([ { packageFile: 'versions.lock', }, { packageFile: 'versions.props', deps: [ { depName: 'org.apache.lucene:lucene-core', depType: 'dependencies', fileReplacePosition: 22, sharedVariableName: 'org.apache.lucene:*', lockedVersion: '1.2.3', managerData: { fileReplacePosition: 22, packageFile: 'versions.props', }, }, { depName: 'org.apache.lucene:lucene-codecs', depType: 'dependencies', fileReplacePosition: 22, sharedVariableName: 'org.apache.lucene:*', lockedVersion: '1.2.3', managerData: { fileReplacePosition: 22, packageFile: 'versions.props', }, }, ], }, ]); }); it('plugin not used due to lockfile not a GCV lockfile', async () => { const fsMock = { 'versions.props': `org.apache.lucene:* = 1.2.3`, 'versions.lock': codeBlock` This is NOT a lock file `, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toBeNull(); }); it('plugin not used due to lockfile missing', async () => { const fsMock = { 'build.gradle': '(this file contains) com.palantir.consistent-versions', 'versions.props': `org.apache.lucene:* = 1.2.3`, }; mockFs(fsMock); const res = await extractAllPackageFiles( partial(), Object.keys(fsMock), ); expect(res).toBeNull(); }); }); });