diff --git a/lib/modules/manager/gradle-wrapper/util.spec.ts b/lib/modules/manager/gradle-wrapper/util.spec.ts index 470adaf837d2b11a1d43e7691e0d2a8d1acf65cb..5087e073d3c7c984986ccbc1f8a6a07eb5ecaa82 100644 --- a/lib/modules/manager/gradle-wrapper/util.spec.ts +++ b/lib/modules/manager/gradle-wrapper/util.spec.ts @@ -5,6 +5,7 @@ import { fs, partial } from '../../../../test/util'; import { extractGradleVersion, getJavaConstraint, + getJavaLanguageVersion, getJvmConfiguration, gradleWrapperFileName, prepareGradleCommand, @@ -45,6 +46,15 @@ describe('modules/manager/gradle-wrapper/util', () => { fs.readLocalFile.mockResolvedValue(daemonJvm); expect(await getJavaConstraint('8.8', './gradlew')).toBe('^999.0.0'); }); + + it('returns languageVersion constraint if found', async () => { + const buildGradle = codeBlock` + java { toolchain { languageVersion = JavaLanguageVersion.of(456) } } + `; + fs.localPathExists.mockResolvedValueOnce(true); + fs.readLocalFile.mockResolvedValue(buildGradle); + expect(await getJavaConstraint('6.7', './gradlew')).toBe('^456.0.0'); + }); }); describe('getJvmConfiguration', () => { @@ -67,6 +77,36 @@ describe('modules/manager/gradle-wrapper/util', () => { }); }); + describe('getJavaLanguageVersion', () => { + it('extract languageVersion value', async () => { + const buildGradle = codeBlock` + java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } + `; + fs.localPathExists.mockResolvedValue(true); + fs.readLocalFile.mockResolvedValue(buildGradle); + expect(await getJavaLanguageVersion('')).toBe('21'); + }); + + it('returns null if build.gradle or build.gradle.kts file not found', async () => { + fs.localPathExists.mockResolvedValue(false); + fs.readLocalFile.mockResolvedValue(null); + expect(await getJavaLanguageVersion('sub/gradlew')).toBeNull(); + expect(fs.readLocalFile).toHaveBeenCalledWith( + 'sub/build.gradle.kts', + 'utf8', + ); + }); + + it('returns null if build.gradle does not include languageVersion', async () => { + const buildGradle = codeBlock` + dependencies { implementation "com.google.protobuf:protobuf-java:2.17.0" } + `; + fs.localPathExists.mockResolvedValue(true); + fs.readLocalFile.mockResolvedValue(buildGradle); + expect(await getJavaLanguageVersion('')).toBeNull(); + }); + }); + describe('extractGradleVersion()', () => { it('returns null', () => { const properties = codeBlock` diff --git a/lib/modules/manager/gradle-wrapper/utils.ts b/lib/modules/manager/gradle-wrapper/utils.ts index fece660f4b0a16a6b89c5e9d34164d6bd05a49fc..a869bbc20b395939a79439b7324597974e4483df 100644 --- a/lib/modules/manager/gradle-wrapper/utils.ts +++ b/lib/modules/manager/gradle-wrapper/utils.ts @@ -2,9 +2,15 @@ import os from 'node:os'; import { dirname, join } from 'upath'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; -import { chmodLocalFile, readLocalFile, statLocalFile } from '../../../util/fs'; +import { + chmodLocalFile, + localPathExists, + readLocalFile, + statLocalFile, +} from '../../../util/fs'; import { regEx } from '../../../util/regex'; import gradleVersioning from '../../versioning/gradle'; +import { parseJavaToolchainVersion } from '../gradle/parser'; import type { GradleVersionExtract } from './types'; export const extraEnv = { @@ -60,6 +66,13 @@ export async function getJavaConstraint( return `^${toolChainVersion}.0.0`; } } + // https://docs.gradle.org/6.7/release-notes.html#new-jvm-ecosystem-features + if (major > 6 || (major === 6 && minor && minor >= 7)) { + const languageVersion = await getJavaLanguageVersion(gradlewFile); + if (languageVersion) { + return `^${languageVersion}.0.0`; + } + } if (major > 8 || (major === 8 && minor && minor >= 5)) { return '^21.0.0'; } @@ -103,6 +116,27 @@ export async function getJvmConfiguration( return null; } +/** + * https://docs.gradle.org/current/userguide/toolchains.html#sec:consuming + */ +export async function getJavaLanguageVersion( + gradlewFile: string, +): Promise<string | null> { + const localGradleDir = dirname(gradlewFile); + let buildFileName = join(localGradleDir, 'build.gradle'); + if (!(await localPathExists(buildFileName))) { + buildFileName = join(localGradleDir, 'build.gradle.kts'); + } + + const buildFileContent = await readLocalFile(buildFileName, 'utf8'); + if (!buildFileContent) { + logger.debug('build.gradle or build.gradle.kts not found'); + return null; + } + + return parseJavaToolchainVersion(buildFileContent); +} + // https://regex101.com/r/IcOs7P/1 const DISTRIBUTION_URL_REGEX = regEx( '^(?:distributionUrl\\s*=\\s*)(?<url>\\S*-(?<version>\\d+\\.\\d+(?:\\.\\d+)?(?:-\\w+)*)-(?<type>bin|all)\\.zip)\\s*$', diff --git a/lib/modules/manager/gradle/parser.spec.ts b/lib/modules/manager/gradle/parser.spec.ts index 101848c6c81937388ff48637b130df729a723dbb..668c6b3ff188cb35c38d9a8a3280ad36791556a7 100644 --- a/lib/modules/manager/gradle/parser.spec.ts +++ b/lib/modules/manager/gradle/parser.spec.ts @@ -2,7 +2,12 @@ import is from '@sindresorhus/is'; import { codeBlock } from 'common-tags'; import { Fixtures } from '../../../../test/fixtures'; import { fs, logger } from '../../../../test/util'; -import { parseGradle, parseKotlinSource, parseProps } from './parser'; +import { + parseGradle, + parseJavaToolchainVersion, + parseKotlinSource, + parseProps, +} from './parser'; import { GRADLE_PLUGINS, REGISTRY_URLS } from './parser/common'; jest.mock('../../../util/fs'); @@ -1108,4 +1113,23 @@ describe('modules/manager/gradle/parser', () => { }); }); }); + + 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); + }); + }); }); diff --git a/lib/modules/manager/gradle/parser.ts b/lib/modules/manager/gradle/parser.ts index 38d710d00cfd00a743661f542841e0de4a0b8365..d9f0f4e6111792c705728bec785f768b863afaa7 100644 --- a/lib/modules/manager/gradle/parser.ts +++ b/lib/modules/manager/gradle/parser.ts @@ -6,6 +6,7 @@ import { qAssignments } from './parser/assignments'; import { qKotlinImport } from './parser/common'; import { qDependencies, qLongFormDep } from './parser/dependencies'; import { setParseGradleFunc } from './parser/handlers'; +import { qToolchainVersion } from './parser/language-version'; import { qKotlinMultiObjectVarAssignment } from './parser/objects'; import { qPlugins } from './parser/plugins'; import { qRegistryUrls } from './parser/registry-urls'; @@ -108,6 +109,13 @@ export function parseKotlinSource( return { deps, vars }; } +export function parseJavaToolchainVersion(input: string): string | null { + const ctx: Partial<Ctx> = {}; + const parsedResult = groovy.query(input, qToolchainVersion, ctx); + + return parsedResult?.javaLanguageVersion ?? null; +} + 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*$`, diff --git a/lib/modules/manager/gradle/parser/common.ts b/lib/modules/manager/gradle/parser/common.ts index 997ac4f42b169f74ef9970c5ce5ca11e480060eb..fffc610c0cac41350278ddbad4cd034cc1dd6ad6 100644 --- a/lib/modules/manager/gradle/parser/common.ts +++ b/lib/modules/manager/gradle/parser/common.ts @@ -313,3 +313,22 @@ export const qKotlinImport = q return ctx; }) .handler(cleanupTempVars); + +// foo { bar { baz } } +// foo.bar { baz } +export const qDotOrBraceExpr = ( + symValue: q.SymMatcherValue, + matcher: q.QueryBuilder<Ctx, parser.Node>, +): q.QueryBuilder<Ctx, parser.Node> => + q.sym<Ctx>(symValue).alt( + q.alt<Ctx>( + q.op<Ctx>('.').join(matcher), + q.tree({ + type: 'wrapped-tree', + maxDepth: 1, + startsWith: '{', + endsWith: '}', + search: matcher, + }), + ), + ); diff --git a/lib/modules/manager/gradle/parser/language-version.ts b/lib/modules/manager/gradle/parser/language-version.ts new file mode 100644 index 0000000000000000000000000000000000000000..38cb4514a15a1deaf77c8333ce0794850b36dbe3 --- /dev/null +++ b/lib/modules/manager/gradle/parser/language-version.ts @@ -0,0 +1,59 @@ +import type { lexer } from 'good-enough-parser'; +import { query as q } from 'good-enough-parser'; +import { regEx } from '../../../../util/regex'; +import type { Ctx } from '../types'; +import { qDotOrBraceExpr } from './common'; + +// (21) +const qVersionNumber = q.tree({ + type: 'wrapped-tree', + maxDepth: 1, + maxMatches: 1, + startsWith: '(', + endsWith: ')', + search: q.num((ctx: Ctx, node: lexer.Token) => { + ctx.javaLanguageVersion = node.value; + return ctx; + }), +}); + +// kotlin { jvmToolchain(17) } +// kotlin.jvmToolchain(17) +const qKotlinShortNotationToolchain = qDotOrBraceExpr( + 'kotlin', + q.sym<Ctx>('jvmToolchain').join(qVersionNumber), +); + +// JavaLanguageVersion.of(21) +const qJavaLanguageVersion = q + .sym<Ctx>('JavaLanguageVersion') + .op('.') + .sym('of') + .join(qVersionNumber); + +// java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } +// kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } +const qLongFormToolchainVersion = qDotOrBraceExpr( + regEx(/^(?:java|kotlin)$/), + qDotOrBraceExpr( + regEx(/^(?:toolchain|jvmToolchain)$/), + q.sym<Ctx>('languageVersion').alt( + q.op<Ctx>('=').join(qJavaLanguageVersion), + q + .op<Ctx>('.') + .sym('set') + .tree({ + type: 'wrapped-tree', + maxDepth: 1, + startsWith: '(', + endsWith: ')', + search: q.begin<Ctx>().join(qJavaLanguageVersion).end(), + }), + ), + ), +); + +export const qToolchainVersion = q.alt( + qKotlinShortNotationToolchain, + qLongFormToolchainVersion, +); diff --git a/lib/modules/manager/gradle/types.ts b/lib/modules/manager/gradle/types.ts index 74dfd059c9823590982a2961979e8677bd4419b0..0a6fbe93a1c0062d9a48cb85ad655d6bca4e5721 100644 --- a/lib/modules/manager/gradle/types.ts +++ b/lib/modules/manager/gradle/types.ts @@ -18,6 +18,7 @@ export interface ParseGradleResult { deps: PackageDependency<GradleManagerData>[]; urls: PackageRegistry[]; vars: PackageVariables; + javaLanguageVersion?: string; } export interface GradleCatalog { @@ -80,6 +81,7 @@ export interface Ctx { globalVars: PackageVariables; deps: PackageDependency<GradleManagerData>[]; registryUrls: PackageRegistry[]; + javaLanguageVersion?: string; varTokens: lexer.Token[]; tmpKotlinImportStore: lexer.Token[][];