Files
Sebastian Poxhofer 58771bb183 refactor(linters): enabled oxlints func-style (#44691)
* refactor(oxlint): enable func-style

Enable the built-in func-style rule as ["warn", "declaration"] so that
named functions are written as function declarations instead of arrow or
function expressions assigned to variables, matching the convention in
docs/development/best-practices.md (Functions section). Inline arrow
callbacks are unaffected; prefer-arrow-callback continues to cover the
callback direction.

Severity is "warn" instead of "error" as a fallback: a repo-wide run
found 196 pre-existing violations (70 in spec files, 126 in production
code), exceeding the 80-violation fix threshold for this change, so no
violations were fixed here. Follow-up work can convert the offenders
(largest clusters: lib/modules/versioning/ruby, lib/util/cache/package,
lib/modules/versioning/maven, lib/modules/versioning/gradle) and then
raise the rule to "error". Note oxlint 1.72 also supports an
allowTypeAnnotation option if typed consts should remain arrows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ci(linter): enforce func-style at error

* refactor(func-style): convert const-arrow functions to declarations in with-cache spec and ruby versioning

* refactor(func-style): convert const-arrow functions to declarations in maven/gradle modules

* refactor(func-style): convert const-arrow functions to declarations in remaining versioning modules

* refactor(func-style): convert const-arrow functions to declarations in platform modules

Discriminated-union closures (ensureCommentRemoval byTopic/byContent
helpers in github, gitlab, bitbucket, bitbucket-server) needed the
narrowed field passed explicitly as a parameter, since function
declarations are hoisted and lose the outer if-narrowing that inline
arrow-function closures previously relied on.

* refactor(func-style): convert const-arrow functions to declarations across util/manager/datasource modules

One case (logger/renovate-logger.ts logOnceFn) is kept as an arrow
function with a documented oxlint-disable: it is stored on
this.logger.once[level] and relies on lexically-bound this, which a
hoisted function declaration would not preserve.

* refactor(func-style): convert remaining const-arrow test helpers to declarations

get-updated.spec.ts: hoisted expectPipCompilePackageAndLockFile to
module scope (it only closes over the module-level pipCompile mock)
and added a documented oxlint-disable for vitest/expect-expect on its
caller test, since oxlint's expect-expect check does not trace
assertions through an externally-defined helper function.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:14:22 +00:00

362 lines
9.1 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);
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<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'),
);