mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 23:11:31 +00:00
4cb4da6bd1
* test(manager): close ansible-galaxy, bun and droneci gaps Adds three extractions: a bun workspace glob that matches no file, a workspace package file that cannot be read, and a drone image continuation that is never terminated. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager/gradle): mark the gradle parser and extract guards Nineteen guards across the gradle family, none of which a test reaches without a build script contrived to defeat the parser. Most are variable lookups and string interpolations that resolved to nothing; the rest cover a catalog fallback already marked unreachable, a registry that is already known, and a dep that did not default to the maven datasource a line earlier. The include/exclude mode chain stays counted, since a coverage-ignore hint does not attach to an `else if`. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager): close kustomize, jenkins and homebrew gaps All five by test, no ignores. A kustomization declaring a local component, a nameless image and a nameless helm chart yields none of them, a jenkins plugin entry without an artifactId is skipped, and a github archive url that is not a tar.gz keeps its suffix in the version rather than having it trimmed. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager/maven): mark the maven extract and update guards Fifteen guards across the maven family. Most check a field the extractor always populates - a dep name, registry urls, a current value. The rest cover a settings.xml carrying no registries, an array that is never falsy, a regex whose groups are present because the line already matched, and three update paths that a buildpack or snapshot bump always satisfies. One is left counted: the `propSource` fallback carries an `istanbul ignore next` that covers the line but not the expression's branch, and widening it would mean rewriting an existing hint. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager/droneci): write the fixture with codeBlock The unterminated image fixture was assembled by joining an array of strings, which hid the shape of the file behind quoting and escapes. It is now a codeBlock literal laid out as the yaml it represents, with a parent key so the indentation the extractor matches on survives the dedent. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> * test(manager): indent the kustomize and jenkins fixtures properly The kustomize fixture sat flush with the statement and listed its components, images and helm charts at the same indentation as their keys. It is now indented one level inside the literal, with the list items nested under the keys as yaml is normally written. The jenkins fixture was an inline string with escaped newlines; it is now a codeBlock literal laid out the same way. Co-Authored-By: Claude Opus 5 <michael.kriese+claude-code@mend.io> --------- Co-authored-by: Claude Opus 5 <michael.kriese+claude-code@mend.io>
364 lines
9.3 KiB
TypeScript
364 lines
9.3 KiB
TypeScript
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<lexer.Token> {
|
|
const tokens = ctx.tokenMap[tokenMapKey];
|
|
if (!tokens) {
|
|
throw new Error(`Expected token ${tokenMapKey} not found`);
|
|
}
|
|
|
|
return tokens as NonEmptyArray<lexer.Token>;
|
|
}
|
|
|
|
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);
|
|
// v8 ignore else -- needs a kotlin import whose prefix resolves to nothing
|
|
if (lastToken && name.startsWith(`${lastToken.value}.`)) {
|
|
const prefix = tokens
|
|
.slice(0, -1)
|
|
.map((token) => token.value)
|
|
.join('.');
|
|
const identifier = `${prefix}.${name}`;
|
|
|
|
// v8 ignore else -- needs a kotlin import whose prefix resolves to nothing
|
|
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<Ctx>().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<Ctx>('.').sym(storeVarToken),
|
|
q.tree<Ctx>({
|
|
type: 'wrapped-tree',
|
|
maxDepth: 1,
|
|
startsWith: '[',
|
|
endsWith: ']',
|
|
search: q.begin<Ctx>().join(qStringValueAsSymbol).end(),
|
|
}),
|
|
),
|
|
0,
|
|
32,
|
|
)
|
|
.handler(stripReservedPrefixFromKeyTokens);
|
|
|
|
// foo.bar["baz"] -> "foo.bar.baz"
|
|
export const qVariableAccessIdentifier = q
|
|
.handler<Ctx>((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<Ctx>(regEx(/^(?:rootProject|project)$/)).op('.'))
|
|
.alt(
|
|
q.opt(q.sym<Ctx>('ext').op('.')).sym(regEx(/^(?:property|getProperty)$/)),
|
|
q
|
|
.sym<Ctx>(regEx(/^(?:extra|ext)$/))
|
|
.op('.')
|
|
.sym('get'),
|
|
)
|
|
.tree({
|
|
maxDepth: 1,
|
|
startsWith: '(',
|
|
endsWith: ')',
|
|
search: q.begin<Ctx>().join(qStringValueAsSymbol).end(),
|
|
})
|
|
.opt(q.sym<Ctx>('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<Ctx, parser.Node>[]
|
|
): q.QueryBuilder<Ctx, parser.Node> {
|
|
return q.alt(...matchers).many(q.op<Ctx>('+').alt(...matchers), 0, 32);
|
|
}
|
|
|
|
export const qValueMatcher = qConcatExpr(
|
|
qTemplateString,
|
|
qPropertyAccessIdentifier,
|
|
qVariableAccessIdentifier,
|
|
);
|
|
|
|
// import foo.bar
|
|
// runtimeOnly("some:foo:${bar.bazVersion}")
|
|
export const qKotlinImport = q
|
|
.sym<Ctx>('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<Ctx, parser.Node>,
|
|
): q.QueryBuilder<Ctx, parser.Node> {
|
|
return q.sym<Ctx>(symValue).alt(
|
|
q.op<Ctx>('.').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'),
|
|
);
|