import type { lexer, parser } from '@renovatebot/good-enough-parser'; import { query as q } from '@renovatebot/good-enough-parser'; import { clone } from '../../../../util/clone.ts'; import { regEx } from '../../../../util/regex.ts'; import type { Ctx, NonEmptyArray, PackageVariables, VariableData, } from '../types.ts'; export const REGISTRY_URLS = { google: 'https://dl.google.com/android/maven2/', gradlePluginPortal: 'https://plugins.gradle.org/m2/', jcenter: 'https://jcenter.bintray.com/', mavenCentral: 'https://repo.maven.apache.org/maven2', }; export const GRADLE_PLUGINS = { checkstyle: ['toolVersion', 'com.puppycrawl.tools:checkstyle'], codenarc: ['toolVersion', 'org.codenarc:CodeNarc'], composeOptions: [ 'kotlinCompilerExtensionVersion', 'androidx.compose.compiler:compiler', ], detekt: ['toolVersion', 'io.gitlab.arturbosch.detekt:detekt-core'], findbugs: ['toolVersion', 'com.google.code.findbugs:findbugs'], googleJavaFormat: [ 'toolVersion', 'com.google.googlejavaformat:google-java-format', ], jacoco: ['toolVersion', 'org.jacoco:jacoco'], jmh: ['jmhVersion', 'org.openjdk.jmh:jmh-core'], lombok: ['version', 'org.projectlombok:lombok'], micronaut: ['version', 'io.micronaut.platform:micronaut-platform'], pmd: ['toolVersion', 'net.sourceforge.pmd:pmd-java'], spotbugs: ['toolVersion', 'com.github.spotbugs:spotbugs'], }; export const GRADLE_TEST_SUITES = { useJunit: 'junit:junit', useJUnitJupiter: 'org.junit.jupiter:junit-jupiter', useKotlinTest: 'org.jetbrains.kotlin:kotlin-test-junit', useSpock: 'org.spockframework:spock-core', useTestNG: 'org.testng:testng', }; export function storeVarToken(ctx: Ctx, node: lexer.Token): Ctx { ctx.varTokens.push(node); return ctx; } export function increaseNestingDepth(ctx: Ctx): Ctx { ctx.tmpNestingDepth.push(...ctx.varTokens); ctx.varTokens = []; return ctx; } export function reduceNestingDepth(ctx: Ctx): Ctx { ctx.tmpNestingDepth.pop(); return ctx; } export function prependNestingDepth(ctx: Ctx): Ctx { ctx.varTokens = [...clone(ctx.tmpNestingDepth), ...ctx.varTokens]; return ctx; } export function storeInTokenMap(ctx: Ctx, tokenMapKey: string): Ctx { ctx.tokenMap[tokenMapKey] = ctx.varTokens; ctx.varTokens = []; return ctx; } export function loadFromTokenMap( ctx: Ctx, tokenMapKey: string, ): NonEmptyArray { const tokens = ctx.tokenMap[tokenMapKey]; if (!tokens) { throw new Error(`Expected token ${tokenMapKey} not found`); } return tokens as NonEmptyArray; } export function cleanupTempVars(ctx: Ctx): Ctx { ctx.tokenMap = {}; ctx.varTokens = []; return ctx; } export function stripReservedPrefixFromKeyTokens(ctx: Ctx): Ctx { const unwantedPrefixes = [ 'ext', 'extra', 'project', 'rootProject', 'properties', ]; while ( ctx.varTokens.length > 1 && // ensures there will be always at least one token ctx.varTokens[0] && unwantedPrefixes.includes(ctx.varTokens[0].value) ) { ctx.varTokens.shift(); } return ctx; } export function coalesceVariable(ctx: Ctx): Ctx { if (ctx.varTokens.length > 1) { ctx.varTokens[0].value = ctx.varTokens .map((token) => token.value) .join('.'); ctx.varTokens.length = 1; } return ctx; } export function findVariableInKotlinImport( name: string, ctx: Ctx, variables: PackageVariables, ): VariableData | undefined { if (ctx.tmpKotlinImportStore.length && name.includes('.')) { for (const tokens of ctx.tmpKotlinImportStore) { const lastToken = tokens.at(-1); if (lastToken && name.startsWith(`${lastToken.value}.`)) { const prefix = tokens .slice(0, -1) .map((token) => token.value) .join('.'); const identifier = `${prefix}.${name}`; if (variables[identifier]) { return variables[identifier]; } } } } return undefined; } export function findVariable( name: string, ctx: Ctx, variables: PackageVariables = ctx.globalVars, ): VariableData | undefined { if (ctx.tmpNestingDepth.length) { const prefixParts = ctx.tmpNestingDepth.map((token) => token.value); for (let idx = ctx.tmpNestingDepth.length; idx > 0; idx -= 1) { const prefix = prefixParts.slice(0, idx).join('.'); const identifier = `${prefix}.${name}`; if (variables[identifier]) { return variables[identifier]; } } } if (variables[name]) { return variables[name]; } return findVariableInKotlinImport(name, ctx, variables); } export function interpolateString( childTokens: lexer.Token[], ctx: Ctx, variables: PackageVariables = ctx.globalVars, ): string | null { const resolvedSubstrings: string[] = []; for (const childToken of childTokens) { const type = childToken.type; if (type === 'string-value') { resolvedSubstrings.push(childToken.value); } else if (type === 'symbol') { const varData = findVariable(childToken.value, ctx, variables); if (varData) { resolvedSubstrings.push(varData.value); } else { return null; } } else { return null; } } return resolvedSubstrings.join(''); } export const qStringValue = q.str((ctx: Ctx, node: lexer.Token) => { storeVarToken(ctx, node); return ctx; }); export const qStringValueAsSymbol = q.str((ctx: Ctx, node: lexer.Token) => { const nodeTransformed: lexer.SymbolToken = { ...node, type: 'symbol', }; storeVarToken(ctx, nodeTransformed); return ctx; }); // https://docs.gradle.org/current/javadoc/org/gradle/api/provider/Provider.html#get() export const qProviderValue = q .tree({ maxDepth: 1, type: 'wrapped-tree', startsWith: '(', endsWith: ')', search: q.begin().end(), }) .handler((ctx) => { if (ctx.varTokens.length > 1 && ctx.varTokens.at(-1)?.value === 'get') { ctx.varTokens.pop(); } return ctx; }); // foo.bar["baz"] = "1.2.3" export const qVariableAssignmentIdentifier = q .sym(storeVarToken) .many( q.alt( q.op('.').sym(storeVarToken), q.tree({ type: 'wrapped-tree', maxDepth: 1, startsWith: '[', endsWith: ']', search: q.begin().join(qStringValueAsSymbol).end(), }), ), 0, 32, ) .handler(stripReservedPrefixFromKeyTokens); // foo.bar["baz"] -> "foo.bar.baz" export const qVariableAccessIdentifier = q .handler((ctx) => { ctx.tmpTokenStore.backupVarAccessTokens = ctx.varTokens; ctx.varTokens = []; return ctx; }) .join(qVariableAssignmentIdentifier) .opt(qProviderValue) .handler(coalesceVariable) .handler((ctx) => { ctx.varTokens = [ ...ctx.tmpTokenStore.backupVarAccessTokens, ...ctx.varTokens, ]; delete ctx.tmpTokenStore.backupVarAccessTokens; return ctx; }); // project.ext.getProperty(...) // extra.get(...) export const qPropertyAccessIdentifier = q .opt(q.sym(regEx(/^(?:rootProject|project)$/)).op('.')) .alt( q.opt(q.sym('ext').op('.')).sym(regEx(/^(?:property|getProperty)$/)), q .sym(regEx(/^(?:extra|ext)$/)) .op('.') .sym('get'), ) .tree({ maxDepth: 1, startsWith: '(', endsWith: ')', search: q.begin().join(qStringValueAsSymbol).end(), }) .opt(q.sym('as').sym('String')); // "foo${bar}baz" export const qTemplateString = q .tree({ type: 'string-tree', maxDepth: 2, preHandler: (ctx) => { ctx.tmpTokenStore.templateTokens = []; return ctx; }, search: q .alt(qStringValue, qPropertyAccessIdentifier, qVariableAccessIdentifier) .handler((ctx) => { ctx.tmpTokenStore.templateTokens?.push(...ctx.varTokens); ctx.varTokens = []; return ctx; }), }) .handler((ctx) => { ctx.varTokens = ctx.tmpTokenStore.templateTokens!; return ctx; }); // foo = "bar" // foo + foo + "${foo}" + "foo" => "barbarbarfoo" export function qConcatExpr( ...matchers: q.QueryBuilder[] ): q.QueryBuilder { return q.alt(...matchers).many(q.op('+').alt(...matchers), 0, 32); } export const qValueMatcher = qConcatExpr( qTemplateString, qPropertyAccessIdentifier, qVariableAccessIdentifier, ); // import foo.bar // runtimeOnly("some:foo:${bar.bazVersion}") export const qKotlinImport = q .sym('import') .join(qVariableAssignmentIdentifier) .handler((ctx) => { ctx.tmpKotlinImportStore.push(ctx.varTokens); return ctx; }) .handler(cleanupTempVars); // foo { bar { baz } } // foo.bar { baz } export function qDotOrBraceExpr( symValue: q.SymMatcherValue, matcher: q.QueryBuilder, ): q.QueryBuilder { return q.sym(symValue).alt( q.op('.').join(matcher), q.tree({ type: 'wrapped-tree', maxDepth: 1, startsWith: '{', endsWith: '}', search: matcher, }), ); } export const qGroupId = qValueMatcher.handler((ctx) => storeInTokenMap(ctx, 'groupId'), ); export const qArtifactId = qValueMatcher.handler((ctx) => storeInTokenMap(ctx, 'artifactId'), ); export const qVersion = qValueMatcher.handler((ctx) => storeInTokenMap(ctx, 'version'), );